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