Files
weichen 4ce8155161 Expand guides with worked examples for intents, runs, and squads
Why: 补充实例让导览可对照实操:意图打分/填槽/多意图裁决给出
intentctl explain 实测轨迹(含样例意图、歧义、优先级 tie-break、
能力目录),任务执行给出工具清单与典型回合走查(含 ClaimGate
三类拦截),小队给出 agent-squad demo 全流程实测与真实工作区
目录树/TASK.md 实拍;并修正事实:默认意图目录为空、插件 overlay
与 registry loader 两种文件形态不同。

Evidence: intentctl explain/catalog 与 agent-squad demo 为 2026-10-08
本地实测输出(命令见各节);锚点仍为 Dev R0 218fc4b1;git diff
--check 通过;五篇互链全部可解析。SRC=SELF。

Impact: 仅文档增补(02/03/04/05 四篇),无代码影响。

Refs: Dev 任务分支 feature/architecture-guides。
2026-10-08 09:33:13 +00:00

19 KiB
Raw Permalink Blame History

Agent 的意图识别:架构、模型与调用链

基准:Agent-System-Dev 组合 218fc4b1ba00b0a03b5bb79f292b3eba6c987a58(R0,2026-10-08 锚点核验)。 行号为该提交下的函数入口位置(个别内部区段为约略范围),随代码演进漂移;文件与函数名是稳定锚点。 internal/ 指 modules/backend/internal/。


1. 一句话结论

意图识别是**"确定性规则漏斗为主体、语义向量与 LLM 为可选慢通道(默认关闭)"的混合 分层架构**:识别主体在独立库 deps/agent-intent(零 LLM 依赖),backend 通过 internal/intentgate 闸门接入,识别结果由 internal/route 统一裁决为 命令 / 工作流 / 普通回合三分支。


2. 识别架构:四级漏斗

核心识别器 FunnelRecognizer(deps/agent-intent/intent/recognizer.go:23, 入口 Recognize 在 recognizer.go:41,内部 recognize 在 :71),四级依次短路:

级 名称 行为 代码
Stage 0 守卫(guard_hit) 守卫域 spec 命中即短路,表达"识别即拒绝" recognizer.go 守卫分支
Stage 1 挂起预期(expectation_hit) 多轮澄清场景:宿主注入 ExpectationProvider 声明上轮预期意图 接口 intent/expectation.go:17
Stage 2+3 规则计分 + 置信度裁决 全部非守卫 spec 线性扫描(intent/registry.go 规则计分与裁决),score = base + (正则命中 ? pattern_bonus : 0),上限 1.0;唯一过阈或前两名分差 ≥ margin(默认 0.1)→ matched,同分比 priority → 否则 ambiguous,无命中 → fallback intent/spec.go:17-29(默认值)、recognizer.go:146 附近同分裁决
Stage 4 慢通道(可选) ambiguous → 语义向量重排 semanticRerank(recognizer.go:292);fallback → LLM 命令生成 generateCommands(recognizer.go:333) 见下节

识别分支共五种(deps/agent-intent/intent/result.go): guard_hit / expectation_hit / matched / ambiguous / fallback。


3. 用什么模型:三层各自不同

层 是否 LLM 实现与模型来源 状态
规则层(守卫/计分/裁决) 否,永不引入 纯 Go 确定性计算。设计文档明确规则层永不引入 LLM(deps/agent-intent/docs/records/2026-09-29-decoupling-and-llm-layering.md §2.1) 主体,默认生效
语义重排(SemanticScorer) 向量模型(非生成式) modules/backend/internal/intentsem/scorer.go:18 EmbeddingScorer:对输入与 spec 的 examples 各取向量算最大余弦相似度。Provider 走 OpenAI 兼容 /embeddings(internal/rag/embed.go),模型/端点来自 settings models.embedding.*;默认阈值 0.8(intent/spec.go) 可选,默认关(settings runtime.intentSemantic)
命令生成(CommandGenerator) 生成式 LLM modules/backend/internal/intentsem/generator.go:18 LightGenerator:走 agentllm.Client.Stream,用轻量档模型(internal/runtime/model.go:84 ResolveLightweightModel,settings models.lightweight.model,缺省回落主 chat 档),MaxTokens 默认 1024;system prompt 在 generator.go:100-129(只暴露 id/描述/槽位键,防提示词注入) 可选,默认关(settings runtime.intentGenerator)
Classifier(LLM 重打分,L2) 生成式 LLM 接口在 intent/classifier.go:9 接口预留,backend 未接线

慢通道装配点:modules/backend/internal/app/app.go:351,357(开关开启才 gate.SetSemantic(...) / gate.SetGenerator(...))。关闭时行为与纯规则版完全一致。

失败安全:agent-intent 库本体零 LLM 依赖,只定义 SemanticScorer (intent/semantic.go:9)与 CommandGenerator(intent/generator.go:14)窄接口由 宿主注入;所有慢通道失败均回退规则结论(fail-open/closed 到 safe 分支),识别永不因 LLM 失败而失败。生成器幻觉有校验兜底:validateGenerated (recognizer.go:368)要求 action.id/槽位键必须都在目录内,否则 fail-closed 回 fallback。


4. 意图目录(Intent Catalog / Schema)

意图定义 IntentSpec(deps/agent-intent/intent/spec.go:74 附近,Go 注册与 JSON overlay 共用):

  • 标识:id(必须 domain.助记 多段,spec.go:122 校验)、domain、version、 description、priority、disabled。
  • matcher(spec.go:32-39):includes_any / includes_all / excludes_any 词表 + patterns 正则 + case_sensitive + boundary(词界匹配,压制中文子串误命中)。
  • slots(spec.go:44-62):key/required/pattern(抽取正则)/group,加 HITL 字段 label/question(建议澄清提问)与校验链 enum/format/normalize/validator。
  • confidence(spec.go:66-70):base 默认 0.7、pattern_bonus 默认 0.2、threshold 默认 0.6。
  • 动作绑定:action(kind ∈ command/workflow/blueprint/handler,intent/command.go:32-43) 优先,其次 metadata["blueprint"]、再次 handler,否则 auto(宿主在命令总线与工作流间裁决)。
  • risk(low/medium/high)+ requires_confirm(透传宿主做授权确认)。
  • examples:示例句数组——语义慢通道按它们算相似度。

配置文件形态(intent/loader.go:12-19):

{ "intents": [ { …IntentSpec… } ], "disable": ["demo.run.status_query"] }

插件可经 intents/*.json overlay 注入(internal/plugins/host.go:216-220 采集)。 样例:deps/agent-intent/examples/specs/demo.json。

识别产物命令集(intent/command.go:11-26):start / set_slot / change_slot / cancel / clarify / chitchat / handoff;规则漏斗只产出 start/set_slot/clarify(deriveCommands 在 command.go:85)。能力目录 Snapshot.Catalog() / CatalogMarkdown() (intent/catalog.go:36,68)把启用意图渲染为 Markdown,供小队规划者/Leader 提示词注入 (deps/agent-squad/adapters/intent/catalog.go 消费)。

4.5 内置与默认意图:默认是空目录

系统默认不内置任何意图:backend 组合根 intentgate.New()(app.go:309)起步是空注册表, g.Match 在空闸门下直接透传(未启用 = 行为回退,所有消息走普通回合)。意图只能经两条 overlay 路径进入,且两种路径的文件形态不同:

路径 文件形态 装载方
插件 overlay(产品路径) 裸数组 [ {…IntentSpec…}, … ] plugins/host.go:216-220 按元素透传 → gate.Rebuild(gate.go:104)逐个 unmarshal 成 IntentSpec
registry/loader(库路径:intentctl、BuildFS) 包装对象 {"intents":[…], "disable":[…]} intent/loader.go:21 overlayDoc,严格 JSON(未知字段报错)

仓库自带两份样例意图(即"默认能看到的意图"上限——装载对应插件/配置后生效):

  • plugins/example/intents/demo.json(插件形态):demo.show(命中"示例/演示",无槽位)、 demo.brief(命中"简报",必填槽位 topic,抽取正则 主题[::]\s*(\S+),缺槽问"主题是什么?")。
  • deps/agent-intent/examples/specs/demo.json(loader 形态):demo.run.status_query (命中"进度/状态"、排除"配置",槽位 run_id,base=0.8/threshold=0.7,handler=taskrun.query_status)、 demo.run.restart(需同时含"重启"+"任务",requires_confirm=true,risk=medium)。

4.6 实测:一句话怎么被打分、填槽、命中

以下全部为 intentctl explain 在样例配置上的真实输出(2026-10-08 实测;例 5/6 的配置目录 = 两份样例意图合并)。打分公式:score = base + (正则命中 ? pattern_bonus : 0)(封顶 1.0), 前两名分差 < margin(默认 0.1,intent/registry.go:55)判 ambiguous。

例 1|命中 + 抽到槽位(输入"查一下任务 run_42 的进度"):

分支:matched
Top:demo.run.status_query 置信度=0.8000 风险=low 确认=false handler=taskrun.query_status
  槽位 run_id="run"
候选(1):
  0.8000  demo.run.status_query        terms=进度
裁决轨迹:
  margin=0.1000
  守卫:未评估
  规则 命中 demo.run.status_query        0.8000 进度
  规则 排除 demo.run.restart             no_positive_match

要点:terms=进度 命中词表加 0 分(base 已含);restart 因 includes_all:["任务"] 缺"重启" 被排除(missing_includes_all)。槽位 run_id="run" 是真实结果——槽位正则 (?:run|任务)[:# ]*([A-Za-z0-9-]+) 的字符类不含下划线,捕获在 run 后被 _ 截断: 规则识别完全确定性,槽位 pattern 的质量决定抽取质量(写 [A-Za-z0-9_-]+ 即可取到 run_42)。

例 2|命中但缺必填槽位(输入"写一份简报",demo.brief 装载时):

分支:matched
Top:demo.brief 置信度=0.7000
  缺失必填槽位:topic
裁决轨迹:
  规则 命中 demo.brief                   0.7000 简报
  规则 排除 demo.run.restart             missing_includes_all:任务
  规则 排除 demo.run.status_query        no_positive_match
  规则 排除 demo.show                    no_positive_match

Missing=[topic] 连同 spec 的 question:"主题是什么?" 一起随 route 进入工作流 run, 由 HITL 输入卡向用户澄清(见调用链第 10 步)。

例 3|槽位填上(输入"主题:核能 写一份简报"):同一意图 matched, 槽位 topic="核能",Missing 为空——正则 主题[::]\s*(\S+) 命中,直接启动。

例 4|什么都不像(输入"随便聊聊今天的天气"):

分支:fallback
裁决轨迹:
  规则 排除 demo.run.restart             missing_includes_all:任务
  规则 排除 demo.run.status_query        no_positive_match

无命中 → fallback;若装载了语义慢通道(runtime.intentSemantic,默认关)会先尝试 embedding 重排,仍不达阈才 fallback。gate 把 fallback 折叠后 route.Decide 判 KindTurn → 普通对话回合。

例 5|能力目录实测(intentctl catalog,这段 Markdown 就是注入小队规划者/Leader 提示词的内容,squad/engine.go:44 注释明示用途):

# 系统能力目录(意图快照 revision=1)

以下能力可直接调用,无需派智能体执行。子任务命中其一时,输出携带 intent_id 的步骤并填齐必填槽位;
必填槽位信息不足时不要编造,改为向用户澄清(优先使用各槽位的提问建议)。

## demo.brief — 写一份简报。必填槽位 topic,没写就问「主题是什么?」。
- 风险:low;执行前需人工确认:否
- 槽位 topic(必填,topic)
  - 提问建议:主题是什么?

4.7 一句话里有多个意图怎么办

意图层的契约是一条输入只产出一个 Decision(top-1),不做多意图拆分。多个候选的 裁决顺序固定为:

  1. 分差裁决:唯一过阈,或第一名领先 ≥ margin → 直接 matched。
  2. 同分且 priority 不同 → 高优先级直接胜出(I5,不标歧)。实测("帮我生成一份报告", 两个意图 base 都是 0.7、词表都命中"报告"):template 设 priority:10 后 分支:matched,Top:demo.report.template 置信度=0.7000。
  3. 分差 < margin(含同分同优先级)→ ambiguous,先走可选语义重排 semanticRerank (recognizer.go:292,默认关)→ 再走 Classifier L2 重打分(backend 未接线)→ 都没有则 保持 ambiguous。实测同配置去掉 priority 后: 分支:ambiguous,候选(2):0.7000 demo.report.generate / 0.7000 demo.report.template。
  4. ambiguous 经 gate 折叠 → KindTurn → 交给普通 LLM 回合:模型在一次 ReAct 循环里 自行处理多个请求(这也是多数"一句话多个事"的实际归宿——意图层只接管高确定性的 命令/工作流类请求)。
  5. 多轮场景:Stage 1 挂起预期(ExpectationProvider)让澄清中的输入优先回到挂起意图; HITL 自由输入命中其他意图时会取消挂起 run 并转轨(runmgr.go:1261 ResolveHITL), 相当于允许用户中途改道。

5. 端到端调用链:用户输入一条消息后发生了什么

# 步骤 调用方 → 被调方 位置
1 HTTP 入口 路由注册 POST /api/conversations/{id}/messages internal/server/server.go:168(注册)、:574 postMessage
2 slash 短路 / 前缀非技能命令 → handleCommand(server.go:756)→ 命令总线,不进意图识别 server.go:623
3 附件摄取 ingest.Parse(name, mime, content) → core.ParsedDocument(与意图识别并行、互不依赖) server.go:643;internal/ingest/ingest.go
4 识别调用 s.recognize(ctx, text)(端口 NLIntentGate,server.go:79-85) server.go:655
5 进入闸门 intentgate.Gate.Recognize internal/intentgate/gate.go:137
6 四级漏斗 FunnelRecognizer.Recognize(ctx, input, reg.Snapshot()) gate.go:144 附近 → deps/agent-intent/intent/recognizer.go:41
7 结果投影 库 Result → MatchResult{Branch, ID, Slots, Missing, Commands};guard/expectation 折叠为 fallback;缺槽位附 spec 的 Question gate.go:148-176
8 统一路由 route.Decide(recognized, route.Env{HasCommand, WorkflowFor, SquadReady}) server.go:656;internal/route/route.go:60
9a 命令分支 KindCommand → s.Bus.Dispatch(Entrance="chat",槽位透传 Params) server.go:701-717
9b 工作流分支 KindWorkflow → mgr.StartClaimedWorkflowRun(缺槽位列表随行) server.go:725;internal/runmgr/runmgr.go:996
9c 回合分支 KindTurn → ClaimConversation 抢槽 → AppendMessageWithRequest 落库 → mgr.StartClaimedConversationRun server.go:679, 727;runmgr.go:980, 991
10 澄清/HITL 工作流缺必填槽位 → 逐个发 HITLCard{Kind:Input, AllowFreeInput} 人工输入卡 internal/wfcompile/run.go:47 起 RunCompiled 槽位补齐段
11 HITL 转轨 POST /api/hitl/{id}/resolve → mgr.ResolveHITL:自由输入以 / 开头 → pivot=command;否则再过 gate.Recognize + route.Decide,命中其他意图则取消挂起 run 并 pivot server.go:883;runmgr.go:1261(转轨判定 :1293 附近)

启动期装配链

# 内容 位置
A1 app.Run 建 gate := intentgate.New() 并 mgr.SetGate(gate) internal/app/app.go:309-310;runmgr.go:142
A2 插件装载回调:pluginHost.OnIntents = overlay → gate.Rebuild(overlay)(fail-closed:解析失败保留上一快照) app.go:324;internal/plugins/host.go:216-220;gate.go:96
A3 开关开启时接线慢通道:gate.SetSemantic(intentsem.EmbeddingScorer{…}) / gate.SetGenerator(intentsem.LightGenerator{…}) app.go:351,357;gate.go:66,75

6. intentgate 与 intentsem 的分工

  • intentgate(internal/intentgate/gate.go):宿主闸门适配器。持 Registry + FunnelRecognizer;负责插件 overlay 装载(Rebuild,:96)、分支折叠(非 matched/ambiguous 一律折叠为 fallback,gate.go:148-155)、补槽位 Question、 类型投影(让 runmgr/route 只依赖本包表面)、慢通道接线(:66,75)。 不做缓存——识别是纯函数 Query,快照不可变无锁读(intent/registry.go)。
  • intentsem(internal/intentsem/):慢通道实现包。EmbeddingScorer 出错原样上抛 (库保持 Ambiguous);LightGenerator 解析失败/模型不可达返回错误(库保持 fallback)。

7. 识别结果的去向与失败路径

  • 识别结果 Result(intent/result.go)是纯内存 Query 结果,不落库(store 无 intent 表)。去向仅三处:① HTTP 响应;② logsys 结构化日志 intent recognized (recognizer.go:62 附近,不记原文);③ 作为 run 启动参数(workflow slots/missing、命令 ID)。
  • ambiguous(重排后仍歧义)→ gate 折叠 → route.Decide 返回 KindTurn (route.go:61-63)→ 走普通 LLM 回合自由对话兜底(不是强制澄清)。
  • matched 但缺必填槽位 → Commands=[start, clarify](intent/command.go:110 附近)→ workflow 路径把 Missing(含建议 Question)传入 run → 发 HITL 输入卡澄清。
  • guard_hit / expectation_hit → 折叠为 fallback → turn。
  • 低置信度(未过 threshold)→ fallback → turn。

route 决策细节(internal/route/route.go:55-114):优先级 命令 > 工作流 > 普通回合;blueprint → KindSquad,但当前不可达—— server.go:664 明确注释 SquadReady 保持 false(per-run entry 覆盖未就位), 小队的实际启动走别的路径(见小队与子 Agent 启动)。 声明目标不可解析时不静默错配,回落 turn 并写 Reason。


8. 辅助工具与测试

  • deps/agent-intent/cmd/intentctl/:只读诊断 CLI(lint 校验配置+词条冲突、 explain 单条输入裁决轨迹(recognizer.go:226 Snapshot.Explain)、 eval 语料评测 Precision/Recall、catalog 输出目录)。
  • deps/agent-intent/conformance/conformance.go:识别器公开契约行为验收 (分支/槽位断言 + 确定性 + 并发冒烟)。
  • deps/agent-intent/catalogcheck/align_test.go:断言库错误码与 Error-Code-System intent 域种子码逐个相等,防两套平行权威。

9. 关键文件清单

文件 职责
deps/agent-intent/intent/recognizer.go 四级漏斗识别器、语义重排(:292)、生成兜底(:333)、Explain 诊断(:226)
deps/agent-intent/intent/spec.go IntentSpec/MatchSpec/SlotSpec/ConfidenceSpec 定义与校验
deps/agent-intent/intent/registry.go 编译、不可变 Snapshot、双源合并、原子 Reload
deps/agent-intent/intent/loader.go JSON overlay 加载
deps/agent-intent/intent/command.go 命令模型 + deriveCommands(:85)展开
deps/agent-intent/intent/catalog.go 能力目录与 Markdown 渲染(:36/:68/:128)
deps/agent-intent/intent/semantic.go generator.go classifier.go expectation.go 四个可注入端口
deps/agent-intent/cmd/intentctl/main.go 诊断 CLI
modules/backend/internal/intentgate/gate.go 闸门适配器(overlay :96、投影、慢通道接线 :66/:75)
modules/backend/internal/intentsem/generator.go scorer.go LLM 命令生成(含 system prompt :100)/ embedding 重排
modules/backend/internal/route/route.go 唯一 Router(Decide :60 三分支裁决)
modules/backend/internal/server/server.go 消息入口 postMessage(:574)、recognize(:655)、resolveHITL(:883)、SSE events(:275)
modules/backend/internal/plugins/host.go 插件 intents/*.json overlay 采集(:216-220)
modules/backend/internal/app/app.go 组合根装配(:309-310、:324、:351/:357)