Agent 能力总览
首页的「Agent 能力」入口在这里展开成可执行的细节。 组织逻辑是四层:模型能碰什么(工具治理)、模型怎么被驱动(循环与预算)、 跑砸了怎么办(时间旅行与回滚)、跨会话留下什么(记忆与技能)。
四层,不是一个循环
大多数「Text2SQL Agent」的叙事是一句话:让模型调用工具、自己纠错、直到满意。这句话是对的, 但它描述的是第二层。一个能长期跑在生产上的问数 Agent,四层缺一不可—— 而且越往下越不该交给模型。
| 层 | 要解决的问题 | 确定性占比 |
|---|---|---|
| ① 工具治理 | 模型能看见哪些工具、越权时发生什么 | 全部——注册与裁剪在构图时完成,运行期零判断 |
| ② 循环与预算 | 怎么驱动、什么时候停、烧多少 | 骨架确定,停止条件交给模型 |
| ③ 时间旅行与回滚 | 这一轮改坏了,怎么回到上一轮 | 全部——版本比对与回滚是纯代码 |
| ④ 记忆与技能 | 跨会话留下什么、下次怎么用上 | 检索是确定性的,写入要过人工确认 |
flowchart LR
TOOLS["① 工具治理
构图时按角色裁剪出可见工具表"]:::entry -- "模型能看见的工具" --> LOOP
MEM["④ 记忆与技能
情节与技能进上下文,写入过确认"]:::entry -- "上下文块 / 注入的 skill" --> LOOP
subgraph LOOP["② 生成循环(ReAct)"]
direction LR
M["模型"]:::gen -- "工具调用" --> T["工具执行
语法校验 / 只读探测 / 规则链"]:::exec
T -- "观察结果" --> M
M -- "finish(SQL)" --> OUT["候选 SQL"]:::gen
end
LOOP -- "每轮落检查点" --> TRAVEL["③ 时间旅行与回滚
版本比对判有没有进步,改坏了回上一轮"]:::act
classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
classDef act fill:#8b5cf614,stroke:#8b5cf699,stroke-width:1.5px;
style LOOP fill:#71717a0d,stroke:#71717a2e,stroke-width:1px
能把判断变成代码的地方,就不要变成提示词。这不是「不信任模型」,而是:确定性规则可解释、可回归测试、且换模型不退化。
所以 join 路径由语义层算、结果对不对由规则链判、这一轮有没有进步由版本比对判——模型只负责它真正不可替代的那件事:把一句话理解成一个查询意图。
十二组能力对照表
下表是十二组能力的索引。前三列是「在哪读」,最后一列指向本页或相邻的详解页。
| # | 能力组 | 一句话机制 | 详解 |
|---|---|---|---|
| 01 | 工具调用与工具治理 | 工具声明可见角色,越权折叠为「未知工具」 | 本页 |
| 02 | 工作流编排 | 节点工厂 + 意图分流两条路 | 查询工作流 |
| 03 | 规划 | 先出计划再写 SQL,schema 段独立预算 | 查询工作流 |
| 04 | 多候选与投票 | 并行多份 SQL → 执行共识投票 | 本页 |
| 05 | 状态管理与时间旅行 | 检查点按存储后端选型,可回放任意一步 | 本页 |
| 06 | 长任务与定时 | 调度器鸭子类型可测,进程内嵌 tick | 运维管理 |
| 07 | 人在环 | 执行前拦 SQL,载荷认不出就默认拒绝 | 本页 |
| 08 | 评估 | 离线回放 + 带方向容差的回归门 | 评测与回归门 |
| 09 | 可观测 | span 树 + 每节点 token 累计 | 可观测性 |
| 10 | 记忆分层 | 门面收口,自动写入一律 pending 或自包含 | 记忆子系统 |
| 11 | 上下文工程 | 分档预算 + 优先级装块 + 块内裁剪 | 本页 |
| 12 | 确定性安全边界 | 只读 AST 防火墙 + 限流 + 审计 | 安全边界 |
① 工具治理:越权不是一道墙,是「没有这个工具」
每个工具在注册时声明自己需要什么权限级别、对哪些角色可见。这份定义在交给模型之前 就按角色裁剪过了:
agent_loop.py:199 ToolSpec agent_loop.py:246 visible_to agent_loop.py:269 ToolRegistry
有意思的是越权时发生什么。运行时不会返回「权限不足」—— 那样等于告诉模型「这里有一扇门,只是你没钥匙」,而模型会开始试探。 它把越权调用折叠成「未知工具」:
模型看到的是「没有这个工具」,于是它换一条路,而不是在墙上撞。这是一个小设计, 但它决定了权限系统是「引导」还是「对抗」。
工具清单按档位懒注册
工具不是一次性全给。简单问题只给 SQL 语法校验和收工工具,标准/复杂档才解锁只读执行探测与规则链校验; 能以只读方式摸到物理 schema 的三个枚举/结构/执行计划工具,还额外要求 analyst 或 admin 角色。 完整清单与各自的注册条件见 LLM 网关 · 工具注册表。
gen_sql.py:1051 build_sql_registry
② 生成循环:护栏不是停止条件
生成节点走一个 ReAct 循环。关键的设计选择是:由模型自己决定什么时候收工, 而不是「跑到没有工具调用为止」或「跑满 N 轮」。
agent_loop.py:454 run_agent_loop agent_loop.py:462 max_rounds
max_rounds 是护栏而不是目标:正常收敛的循环根本碰不到它。真正让模型停下来的,
是它调用 finish 把最终 SQL 作为载荷交出来——见下面的收工协议。
四道预算,全在图上可调
| 预算 | 默认值 | 触顶后 |
|---|---|---|
| tool_timeout_s | 20.0 | 该次工具调用返回超时错误,循环继续 |
| time_budget_s | 120.0 | 整轮停止 |
| max_rounds | 8 | 整轮停止 |
| max_total_tokens | 2500 | 整轮停止 |
graphs.py:1137 tool_timeout_s graphs.py:1140 max_total_tokens
任何一道触顶,或者循环空手而归,都会降级回经典子图——「生成 → 校验 → 重试」 那条确定性的老路。所以 agentic 循环是加速器而不是单点依赖:它失灵时系统还在,只是慢一点。 这次降级会作为一个具名折损记进答案的置信度(−0.10,见答案怎么背书)—— 但只在交付的确实是那条降级路径时:如果共识投票后来采纳了某个没经历过降级的候选, 扣分就落不到它头上。
flowchart LR
A["agentic 循环
自验工具 + 自决收工"]:::gen -- "finish(SQL),载荷过校验" --> OK["候选 SQL → 共识投票 → 执行"]:::entry
A -. "空手而归,或任一道预算触顶" .-> C["降级:经典子图
生成 → 校验 → 重试"]:::err
C -. "具名折损 -0.10(仅当这轮交付的确实是它)" .-> OK
classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
classDef err fill:#dc262614,stroke:#dc262699,stroke-width:1.5px;
linkStyle 1 stroke:#f59e0b,stroke-width:1.5px
收工协议:为什么要有 finish
靠「模型不再调用工具了」来判断结束是脆的:模型可能只是忘了、或者话说了一半。 所以这里把收工做成一个显式工具,模型必须把 SQL 作为参数交出来, 而且载荷要过校验才被接受。
agent_loop.py:320 add_finish_tool agent_loop.py:386 validate_finish
好处是「结束」变成了一个有内容、可校验、可记录的事件,而不是一个静默的推断。
④ 多候选与共识投票
对中等以上复杂度的问题,图上会并行跑多个候选子代理,各自生成一份 SQL,然后由共识节点 实际执行并投票——全票一致就直接采用,单一多数采用多数,分裂则带着分歧点回炉。
投票看的是执行结果而不是 SQL 文本。两份写法不同但结果一致的 SQL,在这个判据下是同一票—— 这正是我们想要的:要的是对的数据,不是对的字面。
顺带的一个细节:相似问法会轮换少样本示例。否则每次都从同一个角度切入,候选之间的多样性会塌掉, 投票也就失去了意义。
⑤ 检查点与时间旅行
每一步都落检查点,落哪种存储由后端决定——PostgreSQL 生产环境走 AsyncPostgresSaver,
本地与测试走 SQLite。
这让「回放任意一步」成为可能:出问题时不必猜,直接从某个检查点重放, 看当时模型拿到的是什么上下文、工具返回了什么。
「这个数字是怎么算出来的」有很多层答案:SQL 在这里、规则链的判定在这里、语义层的口径在这里。但「当时它为什么这么想」只能靠回放。
时间旅行是把 Agent 从「不可复现」变成「可复现」的那一步——没有它,剩下的可解释性都只是事后解释。
⑦ 人在环:默认拒绝
开启后,SQL 在执行前被拦下来交给人确认。这里有一条值得注意的默认值: 载荷认不出来就拒绝,而不是放行。
定时任务走同一条路,但自动放行——否则无人值守的任务会永远卡在确认上。 这两者的区别写在调度器里,不在提示词里。
答案怎么背书:置信度与来源
回答里可以带一个分数和一行来源。这两样东西只做一件事——披露, 它们不进任何条件分支。判据是可验证的:把开关关掉再跑一遍,SQL、行数、裁决、 答案来源逐字节相同。
confidence.py:62 TIER_BANDS answer_source.py:27 AnswerSource confidence.py:325 result_confidence
分数 = 档位基准 + 具名微调
基准看这条答案的来源,四档各占一段互不重叠的分数带:
| 来源 | 是什么 | 分数带 |
|---|---|---|
| certified | 复用了一条已认证的资产——有人具名背书 | 0.95 – 0.99 |
| reused | 复用了 KB 资产(模板或示例),但没人背书 | 0.88 – 0.94 |
| compiled | 语义编译器拼出的权威 SQL | 0.70 – 0.87 |
| generated | 模型生成的(含编译回退与软 MISS 骨架) | 0.05 – 0.69 |
分带刻意不重叠:重叠时「缺口多的 compiled」会落进 generated 的带,分数与档位 就互相矛盾了。不重叠让「一个分数读得回它自己的档」真的成立——这是可以断言的, 不是一句愿望。
加性微调:只在档内加减
微调是几条明写理由的加减,且只在本档的分数带内生效——
越档会让分数与档位打架。每条都带一个机器可读的 name、一个 effect,
以及一句要原样渲染给用户的中文理由:
| 理由(渲染给用户的文案) | 增量 | 什么时候发生 |
|---|---|---|
| 语义编译有 N 个组件未解析,由模型补齐 | −0.05 | 每有一个软 MISS 缺口就扣一次,几个缺口累加几次 |
| agent 自检 N 次通过规则链 | +0.10 | check_result 全部通过 |
| 本轮主候选的 agent 循环未走完,降级到单发生成 | −0.10 | 且 select 没有采纳某个备选候选——备选生成失败时返回空,没走过这条路,不该替它挨罚 |
| 检索到强相关的参考示例 | +0.05 | 检索融合分 ≥ 3.4(约当相似度 0.8);拿不到分不算坏消息,不扣 |
乘性折损:交付结果再打一次折
交付出去的是结果而不是 SQL,所以结果分数在 SQL 分数之上再乘一层折损。 它只记异常项——正常交付一条不扣:
| 理由 | 因子 | 什么时候发生 |
|---|---|---|
| 多候选结果不完全一致(票王得票率 N%) | × 得票率 | select 跑过且票王没拿满票;select 没跑则不扣 |
| 重试预算耗尽后强行交付 | × 0.5 | 状态被置了强行交付位 |
| 结果未通过确定性规则链 | × 0.5 | 跑了规则链且没过——没跑与没过是两件事,只有后者该罚 |
| 查询返回 0 行 | × 0.7 | 裁决为 EMPTY |
| 执行超时被中止 | × 0.7 | 执行期报告了被主动终止 |
| 修正已无进展,提前止损 | × 0.8 | 止损计数非零 |
| 执行期降级,结果被加了行数上限或收窄 | × 0.85 | 与正文的降级提示走同一个判据,不另立一套 |
| 经过 N 轮修正才交付 | × 0.9N | 每轮修正各乘一次 |
| 修正属纯语义重试,比机械错误更可疑 | × 0.9 | 在轮次折扣之上追加的折扣;能活到交付点的最多一次 |
连乘有下限 0.25:没有它,修正十轮的连乘会把分数压到 0,
而 0 是留给「没有可披露的答案」的码位。所以结果不会比它的 SQL 更可信,
两个数天然自洽。
confidence.py:106 _sql_adjustments confidence.py:229 _result_discounts
代码里逐字写着:全部是判断,不是量出来的,不得在任何地方声称它们经过校准。谁把它说成「校准过的置信度」,谁就在编一个不存在的实验。
反过来的一条纪律同样重要:判定不出的,一律倒向最保守的那一档。把编译器的产物标低,用户少得一点信息;把模型编的标高,用户得到一个不该有的信任。两个方向不对称是刻意的——虚低和虚高一样是在编造机制。
怎么开关,显示在哪
开关是 agent.confidence_score(默认开)。它是进程级的:
不在管理端可热更新的设置清单里,要关得改配置再重启——急停要能真的停下来,
所以关掉回退的是整个披露形态(分数回到折叠区、披露面换回枚举行),
而不是半个功能。两个分数照算照写,只在渲染处分流。
⑪ 上下文工程:按优先级装进预算
生成提示不是把所有能塞的都塞进去。上下文按复杂度分档给了 token 预算, 各个信息块按优先级竞争这些预算,块内再按相关度裁剪条目。
| 档位 | 预算 | 典型问题 |
|---|---|---|
| simple | 1500 | 单表、单聚合、无歧义 |
| standard | 2500 | 需要 join、有过滤或分组 |
| complex | 4000 | 多跳、时间窗口、口径歧义 |
context_budget.py:116 assemble_context
优先级顺序本身说明了这套系统的价值排序:少样本示例与用户事实最高, 其次是规则、术语、指标;计划与历史排在中后段;用户画像排最低,只在预算有余量时才进。 完整顺序见 查询工作流 · 上下文预算。