# 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`): ```json { "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 启动](squad-and-subagents.md))。 声明目标不可解析时不静默错配,回落 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) |