亚马逊AWS官方博客

用 Strands Agents SDK 构建确定性数据分析:语义层 + VQR 在 Amazon Bedrock 上的实践

摘要:企业数据分析中,LLM 直接生成 SQL 面临不可复现、不可审计、不可收敛三大挑战。本文提出基于 Strands Agents SDK 和 Amazon Bedrock 的三层确定性架构:语义层将业务术语映射为标准 SQL 片段(毫秒),VQR 知识库通过反馈飞轮缓存验证查询(零 LLM 调用),Agent 层处理长尾问题。明显降低高频查询响应。


1. 引言

本文围绕企业“自然语言问数”场景,提出一种基于 Strands Agents SDK、语义层与 VQR(Verified Query Repository)的确定性数据分析架构。核心思想是将高频问题优先转化为可复用的已验证查询,把业务语义固化为结构化配置,并仅在真正需要时调用大语言模型进行受限推理。

文章系统展开了NL2SQL在生产环境中的三类核心问题:输出的概率性、业务语义理解不足,以及缺少持续积累能力。针对这些问题,文中给出了三层递进架构、语义层建模方法、VQR 飞轮机制、多数据源路由方式以及从10张表到1000张表的扩展路线。

整体方案强调“能不用 LLM 就不用,必须用 LLM 时尽量少用”,以在准确性、可解释性、成本控制与工程可运维性之间取得平衡。

核心观点

  • 不能让 LLM 直接面对数据库,必须通过语义层与已验证查询进行确定性约束。
  • 语义层负责业务语言与数据结构之间的映射,VQR 负责把高频查询沉淀为可复用资产。
  • 系统运行越久,VQR 命中率越高,LLM 调用越少,成本与时延会形成持续优化飞轮。

以下章节将逐层展开,从NL2SQL的核心问题出发,依次深入语义层设计、VQR 飞轮机制、多数据源编排,以及规模化路线。

2. 问题与挑战:NL2SQL 的三个核心问题

企业在尝试”用自然语言查询数据”时,常在概念验证阶段遇到瓶颈。问题根源在于大语言模型的工作方式与企业数据分析需求之间存在结构性矛盾。

问题一:概率性输出与确定性需求的冲突

大语言模型基于概率分布采样。即使设置 temperature=0,同一问题也只是”大概率”输出相同 SQL,而非”确定”输出。更关键的是,当上下文变化(如对话历史、提示措辞、模型版本)时,输出会发生漂移。在生产环境中,这无法接受。例如,财务报表中的”本季度营收”今天查出 1.2 亿,明天查出 1.19 亿,差异源于 SQL 中 WHERE 条件的日期边界处理不同,审计时无法解释。

传统 BI 系统可靠是因为报表背后的 SQL 经过固化、测试和审批。NL2SQL 试图绕过这一流程,代价是将不确定性引入结果层。

问题二:缺乏业务语义理解

“良品率”对应哪张表、哪个字段、什么计算公式?LLM 无从得知,只能推测。这可以理解,因为 LLM 未接触过企业内部的业务术语、数据字典、指标口径。你的 yield_rate 字段,在另一家公司可能叫 quality_ratio,计算逻辑也不同(分母是投料量还是产出量?)。

更复杂的情况包括:

  • 复合指标:NPS 净推荐值 = (推荐者数 – 批评者数) / 总受访者数 × 100。LLM 能写出 SQL,但公式正确吗?如何验证?
  • 口径差异:同样是”DAU”,增长团队和商业化团队的定义可能不同(是否包含内部员工账号?是否排除机器人流量?)
  • 隐含过滤条件:业务上问”北区销售额”,实际暗含”排除退款”、”排除测试订单”,这些规则 LLM 从字面无法识别

虽然可以将规则写入 System Prompt,但随着业务复杂度增加,Prompt 会膨胀失控。在 500 个指标、300 个维度、200 条业务规则的情况下,token 消耗是一方面,更大的问题是 LLM 在超长上下文中注意力分散,关键规则被”遗忘”的概率大幅上升。

问题三:缺乏学习能力,无法积累

第 1000 次查询和第 1 次查询一样,从零推理。每次用户问”上周各大区 GMV 排名”,Agent 都要:理解问题意图 → 推断表结构 → 生成 SQL → 调用数据库 → 格式化结果。

这个过程可能消耗 2000-4000 input tokens、500-1000 output tokens。除了成本,更浪费的是时间:每次查询要等 LLM 推理,P50 延迟 8-15 秒。

这三个问题指向同一结论:不能让 LLM 直接面对数据库。需要在两者之间建立确定性约束层——将业务语义固化,将验证过的查询缓存,只在真正需要推理时才调用 LLM。

3.方案概览

本方案基于 Strands Agents SDK 和 Amazon Bedrock 构建的Agentic架构的数据分析平台。它的核心设计目标是:让自然语言查询像固化报表一样可靠,同时保留 AI 的灵活性。

3.1 架构图

Multi-Agent 数据分析平台·从用户提问到结果返回的完整链路

[图 1. Agentic Data功能架构图]

3.2 方案介绍

整体架构分为六层:用户通过 React 前端发起自然语言提问,请求经 FastAPI 到达 Orchestrator(编排层)。Orchestrator 是纯 Python 代码实现的确定性路由器,不依赖 LLM。它根据问题特征将请求分发到四种执行模式之一:Direct(单 Agent 直连)、Supervisor (LLM 自主调度多 Agent) 、Parallel (多 Agent 并行查询)或 Pipeline (Agent 串行传递)。每个 Agent 由 Strands Agents SDK 驱动,通过 @tool 定义的工具函数绑定到具体数据源。

本方案示例基于汽车行业场景,包含两个数据域:车联网数据(Athena/S3 Parquet):458 万辆车、4.73 亿行驾驶行为记录,涵盖里程、油耗、驾驶评分、行程轨迹等,单表数据量达 GB 级,是典型的超大规模时序分析场景。生产制造数据(RDS PostgreSQL):5 张业务表、3.4 万条记录,覆盖产线排产、订单履约、质检工单等,是典型的关系型事务数据场景。这两个数据域覆盖了企业数据分析的两类典型挑战:海量时序数据的聚合查询(Athena/S3)和关系型业务数据的多表关联(RDS),以及跨数据源的联合分析。需要强调的是,车联网和制造只是演示场景,不是架构的约束。整个平台包括语义层、VQR、Orchestrator、Agent 编排与具体业务领域无关,可以同样的方式接入电商交易、金融风控、物流轨迹或任何 SQL 或数仓可查询的数据源,数据源的接入通过管理后台完成,无需改动代码。

关键创新在于:语义层将业务术语确定性地映射为 SQL 片段;VQR 知识库(Verified Query Repository)通过用户反馈驱动的飞轮机制,将验证过的查询缓存起来,命中时完全绕开 Bedrock,零 LLM 调用、零推理成本。

三层递进架构(VQR → 语义层 → Agent)让系统在确定性和灵活性之间找到了工程上的最优解:能确定的不猜,必须猜的才用 LLM。

3.3 三层确定性递进架构

架构设计的核心原则:能不用 LLM 就不用,必须用 LLM 时尽量少用。

[图 2. 三层确定性递进架构]

每一层都有对应的 AWS 服务承载:

层级 AWS 服务 职责 LLM 依赖
Layer 1 VQR DynamoDB 精确查询缓存
Layer 2 语义层 DynamoDB 指标/同义词字典 轻量(2次调用)
Layer 3 Agent Bedrock LLM 动态推理 完整(5-7次调用)
执行层 Athena / RDS或数仓 SQL 执行
反馈层 DynamoDB 知识积累

架构的本质是一个飞轮:系统运行越久,Layer 1 命中率越高,LLM 调用越少,成本越低,响应越快。这是普通 NL2SQL 方案天生缺失的特性——它们是无状态的,不会因为使用而变得更好。

DynamoDB 在这个架构里承担了三种截然不同的职责:语义层配置存储、VQR 知识库、候选队列管理。选择 DynamoDB 而不是 ElastiCache 或 RDS 的理由是单表读取 <1ms 的 P99 延迟、Serverless 无需容量规划、JSON 文档灵活存储不同形态的数据。

4. Strands Agents SDK——为什么选它

AWS 在 2025 年正式开源了 Strands Agents SDK,定位是”代码优先的 Agent 框架”。在评估用于生产的 Agent 框架时,对比对象通常是 Bedrock Agents 托管方案、LangChain/LangGraph、以及 Strands。

4.1 与 Amazon Bedrock Agents 的协同定位

Strands Agents SDK 和 Amazon Bedrock Agents 是 AWS Agent 生态中的互补选择,而非替代关系。

Amazon Bedrock Agents 是托管方案,优势在于开箱即用:通过控制台配置 Action Groups、绑定 Lambda,5分钟即可运行一个 Agent。对于标准化场景(知识库检索、表单填写、客服对话),Bedrock Agents 提供低代码体验,无需编写 Agent 框架代码或管理对话状态。

Strands Agents SDK 是代码优先方案,适合需要深度定制的场景。数据分析正是这类场景:工具描述需要从 AWS Glue Data Catalog 动态生成、进程内语义层缓存需要毫秒级访问、工具调用前后需要插入校验逻辑、多个 Sub-agent 需要精细协调。这些需求指向需要完全控制 Agent 行为的开发者。

判断标准:如果 Agent 逻辑可以用 Action Group + Lambda 清晰描述,选 Bedrock Agents;如果 Lambda 中的逻辑越来越复杂、需要更灵活的工具编排和状态管理,Strands SDK 更合适。

本文选择 Strands SDK 的具体原因:

from strands import tool
@tool
def semantic_query(
    question: str,
    datasource: str,
    time_range: str,
    granularity: str = "day",
    filters: dict = None,
    top_n: int = 10,
    output_format: str = "table"
) -> dict:
    """从语义层匹配指标并执行查询"""
    # 进程内直接调用,《1ms 开销
    # 直接访问内存中的语义层缓存,无需序列化/反序列化
    matched = _semantic_cache.match(question)
    ...

@tool 定义的工具函数让工具定义回归 Python 原生函数,参数类型和数量无限制。更关键的是进程内调用——工具函数可以直接访问内存中的语义层缓存,无需网络往返,这对于追求毫秒级语义匹配至关重要。

4.2 vs LangChain / LangGraph

虽然 LangChain 是目前最流行的 Agent 框架,拥有丰富生态和活跃社区,但 Strands SDK 在 AWS 原生场景下具有以下优势:

  • 原生 Bedrock 集成: Strands 的 BedrockModel 直接调用 Bedrock API,完整支持 Converse API 特性(流式输出、tool_use、guardrails)。LangChain 的 Bedrock 集成常滞后于新功能发布。
  • 轻量化: Strands SDK 核心依赖极少(boto3 + pydantic),无 LangChain 的深层抽象和大量间接依赖,在 Lambda 或容器中冷启动明显更快。
  • Agent 循环透明可控: Strands 使用显式 Python 代码实现 Agent 循环,支持逐步断点调试。LangGraph 状态机虽强大,但调试复杂 Graph 门槛更高。
  • Hook 机制: BeforeToolCallEvent / AfterToolCallEvent 可在工具调用前后插入自定义逻辑,无需包装 Chain 或重写 Executor。

4.3 关键能力在语义层/VQR 中的应用

Strands 能力 语义层/VQR 中的具体用法
@tool 定义的工具函数 semantic_query、nl2sql_query、pg_query 三个核心工具,参数类型丰富
动态 Docstring 工具描述从 Glue Data Catalog / RDS information_schema 启动时自动生成
SummarizingConversationManager 多轮对话历史自动摘要,防止 context window 溢出
Hook 机制 BeforeToolCallEvent 强制执行 max_tool_calls=8,防止 Agent 陷入死循环
Model 分级 Supervisor Agent 用 Claude Sonnet 做路由决策,DataAnalyst Agent 用 Claude Haiku 执行查询,综合成本降约 70%
Sub-agent 并行 跨数据域查询时,每个数据源独立 Sub-agent,并行执行后汇总

SummarizingConversationManager的实际价值在数据分析场景里,用户可能进行 10-20 轮对话(”上周数据” → “按大区分” → “只看华南” → “对比上上周”),累积的对话历史会迅速占满 context window。Strands 内置的摘要压缩会自动将历史对话浓缩为关键上下文,保留分析意图和已确认的过滤条件,丢弃冗余的中间步骤。

Hook 机制解决了一个生产中常见的实际问题:Agent 有时会陷入”生成 SQL → 报错 → 修改 SQL → 再报错”的循环,每次循环都消耗 token 和时间。通过 BeforeToolCallEvent 可以在每次工具调用前检查计数器,超过阈值就强制终止并返回错误信息,而不是无限重试。

4.4 代码示例:@tool 如何连接语义层

from strands import Agent, tool
from strands.models import BedrockModel
from strands.agent.conversation_manager import SummarizingConversationManager

@tool
def nl2sql_query(sql: str, explain: str = "") -> dict:
    """在 Athena 执行 SQL 查询(Docstring 在启动时从 Glue 动态替换)"""
    result = execute_athena(sql)  # 封装了 start_query_execution + 轮询 + 结果解析
    return {"headers": result.headers, "rows": result.rows}

def create_data_analyst_agent():
    # 启动时从 Glue Data Catalog 拉取表结构,动态替换工具描述
    nl2sql_query.__doc__ = build_docstring_from_glue("analytics_db")

    return Agent(
        model=BedrockModel(model_id="us.anthropic.claude-haiku-4-5"),
        tools=[nl2sql_query, pg_query, semantic_query],
        conversation_manager=SummarizingConversationManager(summary_ratio=0.4),
        system_prompt=build_system_prompt_with_metrics()  # 从 DynamoDB 加载语义层
    )

这段代码展示了三个关键设计:

动态 Docstring:Strands Agent 依赖工具描述决定调用哪个工具。当描述包含准确的表名、字段名和约束条件时,模型生成正确 SQL 的概率显著提升。build_docstring_from_glue() 在 Agent 启动时从 AWS Glue Data Catalog 自动拉取最新表结构元数据(列名、类型、分区键、行数估算)并生成描述。数据工程师在 Glue 注册新表后,Agent 下次冷启动时自动感知,无需代码变更。

语义层注入 System Prompt:build_system_prompt_with_metrics() 从 DynamoDB 加载业务指标定义(公式、同义词、过滤条件)并注入 Agent 的 system prompt。模型生成 SQL 时无需猜测”良品率”对应哪个字段——答案已明确写在 prompt 中。

对话历史摘要压缩:SummarizingConversationManager 在多轮对话中自动将历史浓缩为关键上下文(保留分析意图和过滤条件,丢弃中间推理步骤),防止 context window 溢出。数据分析对话往往很长(”上周数据”→”按大区分”→”只看华南”→”对比上上周”),该机制让 Agent 始终在合理的 context 预算内工作。

5.语义层深入——DynamoDB 存储 + 确定性匹配

5.1 数据模型:DynamoDB config 表结构

语义层的核心是一张 Amazon DynamoDB 表,存储业务指标定义、维度映射、同义词词典和查询模板。

主键设计:

  • Partition Key: config_type (STRING) — 数据类型,如 semantic_layer、datasource
  • Sort Key: config_id (STRING) — 具体配置 ID,如 v1、production

核心数据结构(以”良品率”指标为例):

{
  "metrics": {
    "良品率": {
      "id": "yield_rate",
      "formula": "SUM(qualified_count) * 1.0 / SUM(total_count)",
      "table": "production_daily",
      "sql_template": "SELECT {dimensions}, {formula} AS yield_rate FROM production_daily WHERE ds BETWEEN '{start_date}' AND '{end_date}' {filters} GROUP BY {dimensions}",
      "business_rules": ["total_count > 0 时才计算", "默认排除返工品(rework_flag = 0)"],
      "synonyms": ["合格率", "质量通过率", "pass_rate"]
    }
  },
  "synonyms_flat": {
    "合格率": "良品率",
    "质量通过率": "良品率",
    "pass_rate": "良品率"
  }
}

每个指标包含完整的业务定义:公式、所属表、SQL 模板、业务规则和同义词列表。维度(大区、时间粒度、产品线等)用同样的结构定义,包含列名映射和值映射(如”华南”→region = ‘south’)。

**synonyms_flat 是关键设计:**所有同义词展平成一个字典,匹配时 O(1)哈希查找(即无论同义词数量多少,查找时间恒定),不需要遍历每个指标的同义词列表。一家企业的核心指标通常在50-200个,每个指标按3-5个同义词计算,展平后的字典也就几百个 key,内存占用可以忽略。

DynamoDB 的访问模式

  • 读取语义层配置:GetItem,Partition Key = semantic_layer,Sort Key = v1,< 1ms
  • 更新单个指标:UpdateItem + update_expression,不影响其他指标
  • 多版本管理:Sort Key 用版本号(v1、v2),灰度发布时新旧版本并存

5.2 匹配算法:不用 Embedding,不用向量检索

在做语义匹配时许多团队第一反应是:用 LLM + 向量检索,把指标描述向量化,查询时做语义相似度匹配。这条路在技术上可行,但在数据分析场景里过度复杂,且引入了不必要的不确定性。

维度 向量检索 字符串匹配
延迟 50-200ms(LLM + 向量检索) <2ms(内存字典查找)
确定性 相似度阈值模糊,0.85 和 0.86 可能结果不同 完全确定,匹配即匹配
可解释性 “为什么匹配到这个指标?”——说不清楚 精确到哪个同义词触发了匹配
维护成本 Embedding 模型升级可能改变语义空间 词典维护,业务人员可直接编辑
适用场景 非结构化文档检索、FAQ、知识库 有限集合的精确语义映射

业务指标不是”无限开放”的语义空间,一家企业的核心指标通常在 50-200 个之间,每个指标有明确的同义词集合。这是封闭域问题,不是开放域语义搜索问题。用向量检索解封闭域问题,可能存在过度设计。

匹配算法分三步:

class SemanticMatcher:
     def match(self, question: str) -> dict:
         # 1. 指标匹配:遍历 synonyms_flat 字典,O(1) 查找
         for term, canonical in self.synonyms_flat.items():
             if term in question.lower():
                 matched_metrics.append(self.metrics[canonical])
 
        # 2. 维度匹配:检查问题中是否包含维度名或其同义词
         for dim_name, dim_def in self.dimensions.items():
             if dim_name in question or any(s in question for s in dim_def['synonyms']):
                 matched_dims.append(dim_name)
 
        # 3. 时间范围提取:正则匹配,不用 LLM
         #    "上月" → last_month, "最近30天" → last_n_days(30)
         for pattern, time_type in TIME_PATTERNS:
             if re.search(pattern, question):
                 time_range = time_type
                 break
 
        return {'metrics': matched_metrics, 'dimensions': matched_dims, 'time_range': time_range}

实测数据:97 个指标 + 76 个同义词,在 Python代码上单次 match() 调用耗时 0.3-1.8ms(含正则匹配)。这个延迟对比embedding API 的 50-200ms,差了两个数量级。三步全部在内存中完成,不涉及任何网络调用,结果完全由词典内容决定,不受模型、网络延迟、采样随机性的影响,也就是前面提到的确定性匹配。

5.3 Docstring 动态生成:从 Glue + RDS information_schema 到 Agent 工具描述

大多数 Text-to-SQL 系统的脆弱性藏在一个被忽视的地方:工具描述(tool description)。当 LLM 不知道一张表有哪些列、每列是什么语义时,它只能靠猜,而猜错的代价是一条不可重现的错误 SQL。

Strands Agents SDK 用 @tool 定义的工具函数加 docstring 的方式向模型传递工具能力边界。这个 docstring 不是注释而是 prompt 的一部分,直接影响模型是否选对工具、写对 SQL。手写 docstring 有两个致命问题:数据工程师新建表后需要同步更新代码(几乎不会发生),以及 docstring 和实际 schema 之间的漂移会在最不合时宜的时候爆出来。

解法是启动时从权威元数据源动态生成 docstring,代码里没有硬编码的列名。两个数据源各有对应的权威元数据:

  • Athena 端:从 AWS Glue Data Catalog 拉取。glue.get_table() 返回每张表的列名、数据类型、分区键和行数估算,几行 boto3 代码组装成结构化描述。
  • RDS 端:从 PostgreSQL information_schema.columns 查询。拿到列名、数据类型、可空性,再从 table_constraints 补充主键标注。
 def build_docstring_from_glue(database: str, table: str) -> str:
     """从 Glue Data Catalog 动态生成工具描述"""
     resp = glue_client.get_table(DatabaseName=database, Name=table)
     columns = resp["Table"]["StorageDescriptor"]["Columns"]
     partitions = resp["Table"].get("PartitionKeys", [])
     
    col_desc = "\n".join(f"  - {c['Name']} ({c['Type']})" for c in columns)
     part_desc = "\n".join(f"  - {p['Name']} (分区键)" for p in partitions)
     return f"查询 Athena 表 `{database}.{table}`\n列定义:\n{col_desc}\n分区键:\n{part_desc}"
 
# 注入到 @tool:Agent 下次冷启动时自动感知新增表/字段
 nl2sql_query.__doc__ = build_docstring_from_glue("analytics_db", "driving_behavior")

效果是数据变更和代码变更彻底解耦:Glue 里注册一张新表,或者 RDS 加了一个字段,Agent 下次启动就能自动感知,不需要任何后端代码改动。数据团队和 AI 团队各自迭代,互不阻塞。

5.4 管理后台:零代码配置语义层

把语义层配置写死在代码里是个陷阱:业务定义会变,而代码变更需要走发布流程。正确的做法是把语义层做成运行时可变的数据,持久化在 DynamoDB,通过管理后台 CRUD。

语义层的核心实体只有三类:指标(Metric)、维度(Dimension)、同义词(Synonym)。用 DynamoDB 的 single-table design,pk 前缀区分实体类型(METRIC#dau、DIMENSION#region)。FastAPI 层提供标准 CRUD API,每个写操作后清 Agent 的语义层内存缓存(TTL 5 分钟)。

新数据源接入时的自动给出建议。当管理员在后台填入一个 RDS 连接串后,系统自动扫描 information_schema,根据列名模式和数据类型生成语义层建议:

  • 数值列名匹配 count_*、*_amount、*_rate 等模式 → 建议为指标,推荐聚合函数(SUM/AVG)
  • 字符串列(排除 description、memo 等长文本列) → 建议为维度
  • 高置信度建议一键确认,低置信度的补填业务名称后提交

从连接串到可以自然语言查询,全程不超过 60 秒——这是 information_schema 扫描 + DynamoDB 写入 + 缓存失效的端到端时延。传统 BI 工具接入一个新数据源需要数据工程师写 ETL、BI 工程师建 Cube、业务人员培训——往往是周级别的工作。这里压缩到分钟级,代价是初始建议质量需要业务人员校验,这个人工环节是值得投入的。

6.VQR 深入——反馈驱动的查询知识库

如果说语义层解决的是”LLM 不理解业务语言”的问题,VQR(Verified Query Repository)解决的是一个更直接的经济学问题:对于重复性问题,为什么要反复花钱调用 LLM?

企业用户的查询行为高度集中。一个数据分析平台里,TOP 20% 的问题占据了 70%+ 的查询量。这些高频问题通常也是已经验证过答案质量的问题。理想的处理路径是:第一次用 LLM 生成并验证,之后直接用缓存命中,完全绕开 LLM推理。

VQR 不是简单的查询缓存。查询缓存基于完全匹配,问法稍有变化就失效。VQR 是一个由用户反馈驱动、由管理员审核的查询知识库,支持语义相近的多种问法映射到同一条验证过的 SQL,也就是说在管理员审核过程中,可以加入变体问法。

6.1 存储架构

VQR 在 DynamoDB 中使用两个逻辑分区:

verified_queries:已审核通过、可信赖的查询库。Orchestrator 命中时直接执行其中的 SQL,每条记录的质量由管理员背书。每条记录包含:规范化问题文本、验证过的 SQL、目标引擎(Athena/PostgreSQL 可扩展至其他数据库和数仓)、关键词列表(用于模糊匹配)、变体问法列表(用于同义匹配)、命中计数。

vqr_candidates:待审核候选队列。用户点赞的查询先进入这里,状态为 pending;管理员审核通过后迁移到 verified_queries;执行报错后自动标记为 rejected。

def put_verified_query(question: str, sql: str, engine: str, keywords: list) -> str:
    """将经审核的查询写入 verified_queries"""
    qhash = hashlib.sha256(normalize(question).encode()).hexdigest()[:16]
    vqr_table.put_item(Item={
        "pk": f"verified_queries#{qhash}",
        "question": question, "sql": sql, "engine": engine,
        "keywords": keywords,        # 关键词列表,用于模糊匹配
        "variants": [],               # 同义问法,管理员可追加
        "hit_count": 0,
    })
    return qhash

VQR 的访问模式是高频点查(按哈希命中)和偶发全量扫描(关键词匹配),DynamoDB 的单毫秒点查延迟和按需计费模式完全匹配。

6.2 飞轮机制

VQR 的价值来自持续流转的反馈飞轮,五个环节环环相扣:

环节 1:LLM 生成并执行。用户提问 → Strands Agent 调用 LLM → 生成 SQL → Athena 或 RDS 执行 → 返回结果。这一步耗时约 20-25 秒,调用 LLM 5-7 次。

环节 2:用户点赞触发捕获。前端在每条查询结果旁展示点赞按钮。用户点赞时,后端将问题和 SQL 写入 vqr_candidates,状态为 pending。点赞不等于正确,只是”这个结果看起来对”的信号。

环节 3:管理员审核。管理后台展示 pending 队列,按点赞数和 SQL 复杂度排序。管理员核实 SQL 逻辑后点击确认,系统提取关键词、生成变体列表后写入 verified_queries。

环节 4:Orchestrator 命中跳过 LLM。下次类似问题进来,Orchestrator 在创建 Strands Agent 之前先查 VQR,命中则直接执行 SQL,耗时极短,LLM 调用 0 次。

环节 5:报错自动负反馈。verified_queries 中的 SQL 如果执行报错(表结构变更、权限问题等),Orchestrator 自动标记为 rejected 并回退到 Agent 路径。负反馈是知识库的自我修复机制。

这个飞轮的核心洞察是:正向反馈降成本,负向反馈保质量,两者共同驱动知识库的自进化。

6.3 Orchestrator 级拦截:VQR 命中时不调 LLM

拦截发生在 Strands Agent 初始化之前。Agent 初始化本身会加载工具、预热上下文,即使后续不调用 LLM 也有不必要的开销。VQR 命中时完全不需要 Agent。

匹配分三级:精确哈希匹配(confidence=1.0)→ 变体列表匹配(confidence=0.95)→ 关键词交集匹配(confidence 按交集比例计算)。低于阈值的回退到 Agent。

def orchestrate_query(question: str, vqr_matcher: VQRMatcher) -> dict:
    """Orchestrator 入口:VQR 命中时直接执行 SQL,不创建 Strands Agent"""
    matched, confidence = vqr_matcher.match(question)

    if matched and confidence >= 0.75:
        try:
            result = execute_sql(matched["sql"], matched["engine"])
            vqr_matcher.increment_hit_count(matched["pk"])
            return {"source": "vqr", "confidence": confidence, "result": result, "bedrock_calls": 0}
        except Exception:
            vqr_matcher.mark_rejected(matched["pk"])  # 执行报错 → 自动负反馈

    # VQR 未命中或低置信度:创建 Strands Agent 处理
    return run_with_agent(question)

注意 orchestrate_query 不 import 任何 Strands 组件,VQR 命中路径是纯 Python + boto3 代码,依赖极简,冷启动快,可独立部署为轻量 Lambda。

6.4 成本影响量化

三条路径的成本差异不是线性的,是数量级的差距:

路径 LLM调用次数 端到端耗时 LLM 成本/次(估算)
VQR 命中 ~3s(纯 DynamoDB + Athena/RDS 执行)
语义层匹配 + Agent (路由 + SQL 生成) 5–10s ~$0.005
完整 Agent 推理 (工具调用往返) 20–30s ~$0.03–0.08

VQR 飞轮最值得关注的,不只是效果提升,更是成本结构的持续优化。系统在冷启动阶段对 LLM 依赖较高,但随着查询样本积累、语义层成熟以及命中率提升,越来越多请求可以通过缓存、复用结果或既有语义路径直接完成处理,真正进入大模型推理链路的比例会不断下降。因此,LLM 成本不会随查询量线性增长,反而会在系统稳定运行后逐步收敛。对于查询模式较为固定的企业场景,LLM 最终更像是处理复杂长尾问题的底座能力,其成本也将逐步退化为边际增量成本。

6.5 VQR局限性

也需要明确说明,VQR 并非覆盖所有查询场景的万能组件。它的本质是将“问题”稳定映射到“经过验证的 SQL”。更准确地说,VQR 缓存的是可复用的 SQL 模板或语句,而非查询结果本身;命中时,系统仍会基于该 SQL 实时查询最新数据。因此,VQR 擅长解决的是“问法相对稳定、查询逻辑固定、底层数据持续变化”的高频重复性场景。

这类场景在企业数据分析中非常常见。比如每天查看昨日产量、每周一拉取上周各产线良品率对比、每月汇总不同工厂产能利用率。这些问题的业务含义明确,查询逻辑固定,变化的只是数据本身。对于此类请求,无需每次都依赖 LLM 重新理解、重新生成 SQL;只要能稳定命中已验证的 SQL,就能以更低成本、更高确定性完成查询。这正是 VQR 最有价值之处,也是它最适合承接的核心负载。

但同时也要看到 VQR 的边界。VQR 本身不具备真正的语义理解与推理能力。它能做的是匹配而非理解;能复用既有查询路径,但无法判断新问题在分析意图上是否发生变化。比如“各工厂良品率是多少”和“各工厂良品率哪个最高”,对人来说显然是两个不同问题:前者需要返回全量结果列表,后者则需在全量数据基础上排序并取极值。虽然这两个问题字面高度相似,但查询意图和 SQL 结构并不相同。若仅依赖 VQR 的文本匹配或相似匹配机制,就可能错误地将它们映射到同一条 SQL。这并非 VQR 的缺陷,而是它在设计上本就不承担语义推理职责。

也正因如此,三层架构的设计才是必要的。VQR 负责已知问题的确定性复用,用最小成本处理高频、重复、稳定的问题;语义层负责业务术语、指标口径和字段映射的标准化,为系统提供一致的业务解释基础;Agent 则负责处理未见过的问题、复杂组合问题以及需要多步推理的问题。三者并非彼此替代关系,而是分工明确、能力互补的关系。只有把这一层边界划清楚,整套架构才能既有确定性,又保留足够的灵活性。

从这个角度看,VQR 的价值不在于替代 LLM,而在于把那些本就不需要 LLM 参与的查询请求从大模型链路中剥离出来。它不是要解决所有问题,而是要把最适合标准化、复用化和确定性处理的那部分问题,稳定、高效、低成本地承接下来。正是这种清晰的定位,才使 VQR 能够在整个体系中发挥最大的经济价值和工程价值。

7.端到端示例:一次查询的完整旅程

让我们跟着一个真实问题——”各工厂良品率是多少?”——走完整条链路,看看每个 AWS 服务在哪个环节发力。

7.1 场景 A:VQR 命中(系统的理想状态)

这个问题已经被之前的用户问过,管理员审核通过后写入了 VQR 缓存。

[图3. 场景 A:VQR 命中流程]

7.1.1 示例

命中VQR示例:“各个工厂良品率是多少”,可以看到如下过程:1.用户提问→VQR 文本匹配命中→拿到一条验证过的 SQL ;2.直接执行这条 SQL(Athena 或 PostgreSQL)→拿到查询结果;3.代码把结果拼成 Markdown 表格,返回给前端。VQR命中了,系统跳过了Agent初始化和LLM,由代码把结果拼成Markdown并返回给了前端。全程耗时约 0.5 秒,LLM调用次数:0,成本:$0.00。这不是理论极限,而是 VQR 建立后的正常状态。对于高频问题(良品率、产能利用率、库存周转),VQR 命中率会随使用量快速爬升。

[图4]

7.2 场景 B:VQR 从未命中到命中(系统的学习过程)

同样的问题,第一次被问到时:

[图5. 场景 B:VQR 未命中流程]

这条路径耗时稍长,调用了 LLM如Bedrock Claude Sonnet(语义理解)和 Haiku(SQL 生成)。但它的价值不只是回答了这一次问题,它更为系统贡献了一条永久有效的知识。

7.2.1 示例

[图6]

两个场景的本质差异在于:场景 A 消耗的是过去积累的知识,场景 B 在生产新知识。随着时间推移,场景 A 的比例持续上升,系统的平均成本持续下降——这就是 VQR 飞轮的核心逻辑。

8.规模化路线——从 10 张表到 1000 张表

许多 NL2SQL 项目在从 Demo 到生产的路上遇到大量挑战。Demo 阶段精心挑选了 10 张表,准确率接近完美。但当业务方要求接入全域数据时,表数量从 10 变成 50、200、1000,整个系统开始出现裂缝:context 溢出,误匹配率上升,响应变慢。

规模化不是一个技术问题,首先是一个架构决策问题:什么阶段,用什么策略。

阶段 表数 核心方案 主要 AWS 服务 典型挑战
初期 ≤10 全量 Docstring 注入 Glue + RDS
中期 10–50 场景分片 DynamoDB(场景配置) 场景边界模糊
远期 50–200 两阶段选表 Glue Data Catalog API 第一阶段召回率
极端 200–1000+ 语义层外置 考虑 Cube.js 替换自建 维护成本 vs 能力

8.1 初期阶段:全量 Docstring(≤10 张表)

把所有表的完整定义(列名、类型、业务含义、示例值)塞进 system prompt。这个方案简单粗暴,但在 10 张表以内效果最好——模型有完整上下文,生成的 SQL 准确率高。

这个阶段的唯一工作是把 Docstring 写好,从 Glue Data Catalog 自动拉取表结构,再由人工补充业务注释。

 import boto3
 
glue_client = boto3.client("glue")
 
def build_full_docstring(database: str) -> str:
     """从 Glue Data Catalog 拉取所有表定义,构建完整 Docstring"""
     tables = glue_client.get_tables(DatabaseName=database)["TableList"]
     docstrings = []
     for table in tables:
         cols = [
             f"  - {c['Name']} ({c['Type']}): {c.get('Comment', '暂无注释')}"
             for c in table["StorageDescriptor"]["Columns"]
         ]
         docstrings.append(
             f"表名: {table['Name']}\n"
             f"描述: {table.get('Description', '暂无描述')}\n"
             f"列定义:\n" + "\n".join(cols)
         )
     return "\n\n".join(docstrings)

8.2 中期阶段:场景分片(10–50 张表)

50 张表的完整 Docstring 大约占用 8000–12000 tokens,每次 Agent 调用都要携带这些 context,成本和延迟都开始不可忽视。

解决方案是按业务场景做分片设计:将表分组为”生产质量”、”驾驶行为”、”售后服务”等场景,每个场景只包含相关的 5-10 张表。场景配置存在 DynamoDB 里,查询进来时先识别场景,再只加载对应场景的 Docstring。

 def load_scenario_tables(scenario: str) -> list:
     """从 DynamoDB 加载指定场景的表列表"""
     table = dynamodb.Table("scenario_config")
     item = table.get_item(Key={"scenario_id": scenario})["Item"]
     return item["tables"]  # 例如 ["production_stats", "quality_checks", ...]
 
def classify_scenario(question: str) -> str:
     """用轻量级关键词匹配识别场景,避免额外调 LLM"""
     keywords = {
         "production": ["良品率", "产能", "批次", "生产线"],
         "driving": ["里程", "驾驶", "行程", "速度"],
         "aftersale": ["售后", "维修", "工单", "投诉"]
     }
     for scenario, kws in keywords.items():
         if any(kw in question for kw in kws):
             return scenario
     return "general"  # 回退到全局场景

场景分片的最大风险是边界模糊:一个跨场景的问题(如”驾驶里程高的车辆售后情况”)会让场景识别失准。应对策略是保留一个”general”兜底场景,包含所有表的轻量摘要(只有表名和一句话描述),让模型先确认场景再深入查询。

8.3 远期阶段:两阶段选表(50–200 张表)

200 张表的规模下,即使场景分片也难以维持清晰边界。这时需要引入两阶段选表:

  • 第一阶段(粗选):从 Glue Data Catalog 动态拉取所有表的元数据摘要,用 BM25 或关键词索引选出候选表(5-10 张)。
  • 第二阶段(精选):只把候选表的完整 Docstring 注入 Agent context,生成 SQL。

第一阶段完全在代码层完成,零 LLM 调用;第二阶段的 context 始终控制在合理范围内。

8.4 极端阶段:语义层外置(1000+ 张表)

当表数量超过1000时,自建语义层的维护成本难以承受:列注释需人工维护,同义词词典需持续更新,跨部门命名冲突需要协调。此时应考虑引入专业语义层工具(如 Cube),将业务指标定义、权限控制、缓存策略交由专用工具管理。Strands Agent 只需调用语义层的 REST API 获取 SQL,无需直接接触原始表结构。对于拥有1000张表的企业,为语义层专门投入是值得的。

9.结论

我们用五章的篇幅构建了一套完整的确定性数据分析系统。有三个结论值得单独提炼。

第一,语义层是 Strands Agent 和数据库之间的确定性桥梁。 NL2SQL 的核心挑战从来不是生成 SQL 的技术,而是生成正确 SQL 的概率。语义层通过 DynamoDB 存储业务定义,在 2ms 内返回确定性的表匹配结果,把”LLM 猜测哪张表”变成了”查字典找答案”。这个设计让系统的准确率从依赖模型能力,变成依赖语义层的质量——而后者是可以被人工审核和持续改进的。

第二,VQR 让系统越用越便宜。 传统的 AI 应用,每次调用的成本是固定的;VQR 机制引入了”知识积累”维度——每一个被审核通过的问题,都是一次永久性的成本压缩。高频问题命中 VQR 时,LLM调用次数为零,成本为零。从架构上实现”越用越便宜”。

第三,Strands Agents SDK + AWS 原生服务 = 最小可行架构。 方案没有引入向量数据库、没有搭建 RAG pipeline、没有额外的中间件层。DynamoDB 承担语义层存储,Glue 提供表元数据,Athena 和 RDS 处理各自擅长的查询——每个 AWS 服务都在做它本来就擅长的事,Strands Agents SDK 只是把它们串联起来的胶水。

➡️ 下一步行动:

相关产品:

  • Amazon DynamoDB — 无服务器分布式 NoSQL 数据库
  • Amazon Bedrock — 用于构建生成式人工智能应用程序和代理的端到端平台
  • Amazon Glue — 简单、可扩展的无服务器数据集成
  • Amazon RDS — 完全托管的关系数据库服务
  • Amazon Athena — 使用 SQL 在 S3 中查询数据

相关文章:

10.下一步演进

当前 VQR 采用的是一种非常激进、也非常高效的设计:一旦命中,就直接绕过 LLM,执行已验证的 SQL,并按模板生成回答。这样做的优势很明显,成本低、响应快、结果稳定,但代价是灵活性不足。它适合处理“已知且重复”的问题,却难以应对字面相近但语义不同的提问,例如“各工厂良品率是多少”和“良品率最高的是哪个工厂”。

更值得进一步探索的方向,不是让 VQR 一味替代 LLM,而是让它成为 LLM 的高质量参考输入。也就是说,验证查询不再只是一个命中后直接执行的终点,而是作为已验证的查询样本注入模型上下文,帮助 LLM 更快理解用户意图、继承正确的查询模式,并在此基础上完成 SQL 生成和自然语言回答组织。这样一来,VQR 提供的是确定性经验,LLM 负责的是语义理解和灵活推理,两者形成协同,而不是互相替代。

后续版本会沿着这个方向演进:当 VQR 命中后,不再直接结束链路,而是将对应的验证 SQL 作为参考样本注入 Agent Prompt,由 LLM 结合当前问题生成最终 SQL,并组织最终回答。这样会引入一次轻量级的模型调用,但换来的,是更强的语义理解能力和更自然的回答效果。届时,VQR 的角色也会随之升级:它不再只是一个“精确匹配的缓存层”,而会进一步演进为一个“带有领域知识的推理加速层”。

参考资料

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

王维超

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的云计算方案的架构设计,同时致力于亚马逊云科技云服务在汽车行业的应用和推广。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用

Groupable Contents 模板转换

选择Word文件: