Skip to content

fix: issues#100 protocol routing drops - #105

Open
LemonC19 wants to merge 1 commit into
datagallery-lab:mainfrom
LemonC19:fix/protocol-routing-context
Open

fix: issues#100 protocol routing drops#105
LemonC19 wants to merge 1 commit into
datagallery-lab:mainfrom
LemonC19:fix/protocol-routing-context

Conversation

@LemonC19

@LemonC19 LemonC19 commented Aug 7, 2026

Copy link
Copy Markdown

问题概述
修复 issues#100

短后续提示如 再次尝试("try again")被路由到 general-task,因为协议分类器只看到孤立的后续文本,没有会话上下文。执行 agent 随后拿到会话历史并尝试调用数据工具,但 general-task 拒绝所有数据动作,导致运行退化为反复的 ACTION_NOT_ALLOWED_IN_PHASE 拒绝,最终只以一段文字借口完成。

本 PR 给分类器喂入一个紧凑的路由上下文——上一轮用户查询、上一轮运行协议及终态、已选 skill IDs、已选 datasource——让延续 data-analysis 工作流的后续提示能继承正确的协议。

根因
createRunProtocolBoundary 路由时只用当前 userInput:

analyticIntent(userInput) 是唯一的确定性来源——再次尝试 不含分析关键词,不产生候选。
classificationInput: { userText: input.userInput }——LLM 分类器只看到孤立的模糊文本 → 低置信度 → 回落 general-task。
执行 agent 随后拿到完整会话历史(governedMessages)尝试调用数据工具,但 general-task 在所有阶段都排除 DATA_ACTIONS → ACTION_NOT_ALLOWED_IN_PHASE。这就是 context split:分类器看不到历史,agent 看到历史,governor 又拒绝。

改动
紧凑路由上下文(run-protocol-boundary.ts)—— 在 CreateRunProtocolBoundaryInput 上新增可选字段 routingContext,携带 previousQuery / previousProtocol(含 terminalStatus)/ selectedSkillIds / selectedDatasourceId。这些字段被塞进 classificationInput,让分类器看到上一轮意图,而不只是孤立的后续文本。

分类器 prompt(model-protocol-classifier.ts)—— prompt 现在显式描述各路由上下文字段,并指示 LLM:当当前查询是弱后续(继续/重试/再试/try again…)且上一轮用了 data-analysis 时,倾向于延续 data-analysis,除非用户明确切换了任务主题。

一致性 guard(run-protocol-boundary.ts)—— 路由解析后,若选中 general-task 但本次 run 暴露了数据工具(inspect_schema/run_sql_readonly/…)且选了 datasource,则发 protocol.route.consistency.warning 事件(PROTOCOL_TOOL_POLICY_MISMATCH)。这是 warning,不是硬失败——agent 可经现有 protocol_handoff 工具自行修正。

API 装配层(server.ts、run-agent-assembly.ts、resolve-routing-context.ts)—— 装配 agent 前,通过 RunRepository.findPreviousRunBySession(新增)查本 session 的上一条 run,经 MetadataProtocolStateStore.find 读其协议快照,连同已选 skill IDs / datasource id 组装 routingContext。

Metadata 查询(metadata/index.ts)—— RunRepository.findPreviousRunBySession 返回本 session 最近一条终态 run(completed/failed/canceled),排除当前 run。

为什么不做确定性关键词继承
早期迭代考虑过用确定性"弱后续"关键词清单(继续/重试/再试…)产生高优先级候选。这个方案被否决了——用正则关键词去抢分类器的活很脆弱,中文变体(再来一次、再跑一下、接着上次…)很容易漏。既然分类器本身就是一次 LLM 调用,给它路由上下文让它自然处理这些变体更合理。

向后兼容
routingContext 完全可选。现有 14 个 boundary 测试无需改动全部通过。
恢复路径(persistedState / PROTOCOL_SEGMENT_RESTORED)和显式协议路径(explicitProtocol)未改动。
测试
run-protocol-boundary.test.ts +4:弱后续带 routingContext → data-analysis;不带 → 仍 general-task(回归保护);一致性 guard 触发 / 不触发。
model-protocol-classifier.test.ts +1:prompt 正确展示各路由上下文字段。
run-previous-by-session.test.ts +3:findPreviousRunBySession 查询行为。
全量:agent-runtime 164 passed、metadata 6 passed、api 16 passed。

@LemonC19

LemonC19 commented Aug 7, 2026

Copy link
Copy Markdown
Author
image web测试通过

@wangjk9527 wangjk9527 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:Request changes。建议重写路由与完成模型,而不是继续修补当前 routingContext + warning 方案。

本 PR 能降低部分「再次尝试」被误分到 general-task 的概率,但 Issue #100 描述的是确定性的 session intent/state 延续问题,不应由 LLM classifier 和 confidence threshold 决定。分类失败或置信度低于阈值时仍会回退 general-task,因此核心不变量没有建立。

阻断问题:

  1. 只继承协议标签,没有继承任务语义。 run-protocol-boundary.ts 的 requirements 提取仍只看当前 userInput 或显式协议;「再次尝试」即使被分类成 data-analysis,也不会恢复原 intentText / requirements,semantic query 仍是这句弱后续。协议名延续了,分析契约没有延续。
  2. protocol/tool mismatch warning 不纠正状态。 warning 既不会改变路由或工具策略,也没有进入 Agent 的 governedMessages / 恢复上下文;独立 warning 事件还绕过 state-store journal 写入。依赖 Agent 事后自行调用 protocol_handoff 不是可靠恢复机制。
  3. 假成功路径仍存在。 数据动作被协议拒绝后,只要产生 assistant message,completeProtocolRun 仍会自动执行 general.answer.commit,随后无条件 forceTerminal: truepartial 最终又由 finalizer 写成 metadata completed。因此“工具未执行但文字解释后成功结束”没有被堵住。
  4. 测试没有证明真实路由正确。 新测试中的 classifier stub 本身实现了「看到 previousProtocol 就返回 data-analysis」,只能证明字段传递,不能覆盖真实 classifier 失败、低置信度回退、requirements 恢复或 completion 假成功。

建议当前开发期直接采用更简单的模型:

  • 持久化权威 SessionIntent { protocolId, intentText };同 run resume 恢复 run state,新 run follow-up 确定性继承 session intent。classifier 仅用于无 intent 的首次请求。
  • 使用 intentText 初始化 data-analysis requirements 和 semantic query,而不是弱 follow-up 文本。
  • 显式 protocol_handoff 才更新 session intent;handoff 的 unresolved goals 由服务端 protocol state 计算,并同步重建目标协议的工具策略。
  • 先确定协议,再暴露对应工具;删除 mismatch warning、历史 routingContext 字段和 follow-up confidence 分支。
  • completion 使用硬门禁:必要动作被拒或 requirements 未满足时不得 completed;删除无条件 forceTerminal,assistant 文字只能说明终态,不能把失败/partial 转成成功。

关键测试建议:

  • data-analysis 失败后发送「再次尝试」,不调用 classifier,继承原 intent/requirements,数据工具可直接执行;
  • 已有 session intent 时 classifier 失败或低置信度不影响路由;
  • 数据工具被协议拒绝后,即使输出解释文字,run 也不能 completed
  • handoff 后 session intent、协议状态和工具集合原子一致;
  • crash/replay 后 Agent 与 UI 看到同一 journal 状态。

当前改动约 444 行,但继续修补仍需重做 session state、requirements、工具装配和 completion。无需兼容旧方案的前提下,重写会更短、更明确,也更容易证明正确。

@iancaoo iancaoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

代码评审 — 正确性风险

基于 正确性 — 真正的风险 部分的评审意见,以下是 5 条需要在合并前考虑的正确性问题。核心路由上下文机制是良好的通用设计,以下为正确性缺口与加固建议。详细分析见行内评论。

# 问题 严重性
1 "最近的终态 run" ≠ 意图祖先 高 — 可能重新引入/加剧该 bug
2 不一致的 terminalStatus 词汇表 中 — 误导分类器
3 一致性 warning 无消费者 中 — 描述与实现不符
4 一致性 guard 误报 中 — 噪音
5 reasonCodes 非契约 低 — 仅测试安全隐患

* so callers without history are unaffected.
*/
export const resolveRoutingContext = (input: ResolveRoutingContextInput): RoutingContext | undefined => {
const previousRun = input.metadataStore.runs.findPreviousRunBySession({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正确性风险 #1 — "最近的终态 run" ≠ "用户正在延续的 run"

findPreviousRunBySession 返回最近的终态 run。考虑以下序列:

  1. 用户:“分析订单数据” → data-analysis, completed
  2. 用户:“谢谢” → general-task, completed
  3. 用户:“再试一次” → 应当继承第 1 轮的 data-analysis,但 resolveRoutingContext 把第 2 轮视作 previousProtocol = general-task 喂给分类器。分类器看到 previousProtocol=general-task 很可能维持 general-task —— 这加剧了该 bug,而非修复。

设计把"最近的终态 run"与"承载用户正在延续意图的 run"等同起来,但二者并不总是相同。测试套件只覆盖了前一轮是 data-analysis 的情况,没有覆盖这条间隙的 general-task 场景。

建议: 要么向后遍历终态 run 直到遇到非默认协议,要么在 session 元数据里持久化一个粘性的 lastNonDefaultProtocol,让后续提示能可靠继承。并补一个测试:前一轮是 general-task 而其祖先是 data-analysis

protocolVersion: string;
terminalStatus?: string;
} => {
const terminalStatus = state.status === "terminal"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正确性风险 #2buildPreviousProtocol 喂给分类器的词汇表不一致

const terminalStatus = state.status === "terminal"
  ? state.terminalDecision?.status
  : state.status;

三个问题:

  • state.status === "terminal"terminalDecisionundefined 时,terminalStatusundefined → 被整个丢弃。分类器看到 上一轮协议: data-analysis无终态,丢失信号。
  • state.status 不是 "terminal"(例如 "active""waiting""handed_off")时,回落到字面状态字符串 —— 分类器会看到 终态=active终态=handed_off,而对终态 run 看到的是 终态=completed/degraded/partial/continue/failed。这是两套互不兼容的词汇表被喂给同一个 LLM。
  • terminalDecision?.status 可以是 "continue"ProtocolCompletionDecision 的一个变体),不是终态,却被呈现为 终态=continue

字段叫 terminalStatus 但可能携带 "active" —— 有误导性,且 LLM 无法获知这些状态的含义。

建议: 规范化到一套一致的词汇表。要么始终映射到完成决策的状态(非终态 run 则省略该字段),要么在 prompt 里描述每个状态的含义。不要向分类器展示 终态=active

// while being governed by a protocol that rejects every data action"). When the
// resolved protocol is general-task but the run exposes data tools and a selected
// datasource, emit a warning so the agent can self-correct via protocol_handoff.
if (route.definition.id === "general-task" && exposesDataToolPolicy(input) && input.routingContext?.selectedDatasourceId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正确性风险 #3 & #4 — 一致性 guard

#3 — warning 事件似乎无消费者: guard 发出 protocol.route.consistency.warning 携带 PROTOCOL_TOOL_POLICY_MISMATCH,PR 描述声称"agent 可经 protocol_handoff 自行修正"。但 server.ts 与 assembly 层没有该事件的消费者 —— 它被发出后即丢弃。route.warnings 携带该 code,但路由已解析完毕,boundary 层没有任何东西读 route.warnings 触发切换。

如果下游确实没有自行修正逻辑(PR 未展示),那 guard 纯粹是可观测性 —— 这没问题,但"agent 自行修正"的说法在 PR 范围内未证实。建议: 确认 protocol_handoff 路径是否真的读取此 warning,否则把描述里的说法从"agent 自行修正"降级为"为可观测性发出 warning",直到自行修正能力就位。

#4 — 合法非数据任务也会触发误报: guard 在任何选中 datasource 且解析为 general-task 时触发。但用户可能选中一个数据源然后问"这个数据源有哪些表?" —— 这是合法的 general-task(解释而非分析),guard 却会发出 PROTOCOL_TOOL_POLICY_MISMATCH 警告。启发式过粗:datasource 被选中 + 数据工具存在 + general-task 并不必然意味着分类错误。建议: 至少确认该 warning 作为可观测性噪音可接受(非阻塞);若 general-task 上的误报率高,考虑收紧触发条件(如要求分类器置信度低于某阈值)。

`候选集合: ${input.candidates.map((item) => `${item.protocolId}@${item.protocolVersion}`).join(", ")}`,
...(fields.length > 0 ? ["分类输入:", ...fields] : [`分类输入: ${JSON.stringify(input.value)}`]),
"只返回一个 JSON 对象,不要 Markdown。字段为 protocolId、protocolVersion、confidence、reasonCodes。",
'格式示例: {"protocolId":"data-analysis","protocolVersion":"1","confidence":0.91,"reasonCodes":["INHERITED_PRIOR_PROTOCOL"]}',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正确性风险 #5INHERITED_PRIOR_PROTOCOL 不是可强制契约

prompt 示例建议用该 code,stub 分类器也返回了它,测试 expect(boundary.route.reasonCodes).toEqual(["INHERITED_PRIOR_PROTOCOL"]) 对 stub 通过。但真实 LLM 可返回任意 [A-Z_]+ code —— schema 允许。因此 reasonCodes 在生产中不是路由来源的可靠信号;它只是信息性的。

这没问题,但测试给人一种错误的确切感。建议: 要么在文档中注明 reasonCodes 是建议性而非可强制,要么从固定集合中约束它。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants