Files
Agent-System-Documents/docs/guides/intent-recognition.md
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

293 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) |