跳转到内容

RFC-0015:网关 AI 问题事件

项目 值
状态 Superseded(2026-09-16)— P0 曾落地(2026-09-14);/api/agent 对话回路随 S9 之后删除,agent.problem 行不再产生,被拒计划与其代码在浏览器的步进轨迹里(有界步方案)
文档版本 0.4.1
创建/最后修改 2026-09-13 / 2026-09-16
事件 agent.problem(aira.gateway-log/0.1 里的一种新 event)
上位路线 生产运行条件 §4
发布准入 上线计划 G1、G2
依赖 服务说明
授权边界 本文不授权部署、付费模型调用或 Git 提交

网关只在一次 AI 请求里出了能定位的问题时,往已有的结构化日志多写一条 agent.problem:哪个任务、哪个模型、 第几步、哪一类问题、哪个码,再加一个不超过 200 字符的定位线索。成功的请求不多写一个字。没有新表、新绑定、 新路由、新脚本,浏览器不改。够用了就停在这里;不够用的证据出现前不扩。

1. 网关已经看到的、只差记下来的

Section titled “1. 网关已经看到的、只差记下来的”

逐项核对 deepseek-agent.ts、http-handler.ts、浏览器 agent-client.ts,定位问题所需的信息网关此刻都在手里:

要回答的问题 信息在哪
是哪个任务 请求体 id——浏览器用 taskId 构造 chat,chat.id === taskId;已校验,未记录
用的什么模型 resolvedModel,已进 gateway.request.completed
宿主拒了什么 续传请求 messages 最后一条 assistant 消息里的 tool-aira_compile_exact_plan 部件:output.status = needs-repair 与 output.diagnostic.{operation, code, message};浏览器已原样送回,网关校验条数后原样交给 SDK。SDK 在工具结果续传时沿用同一条 assistant 消息,所以这条消息累积整个任务的每一步,只有最后一个 step-start 之后的部件是新的
计划格式错在哪 onError 里已用公开 schema 算出 EXACT_CAD_PLAN_INVALID: … 的字段路径,只回给模型
是否编造了工具名、参数不合 schema onStepEnd 的 toolCalls 里 invalid: true 的调用带着 NoSuchToolError / InvalidToolInputError 本身
是否没调工具就停了 onStepEnd 的 finishReason 与 toolCalls
查库有没有查到 查库工具在服务端执行,toolResults.entries / redirected 在 onStepEnd 可见
是不是被预算截停 三条 stopWhen 与 prepareStep
provider 错、流中取消 已作为 errorCode 进请求日志

看不到的只有“两次请求之间被浏览器取消”,本文不处理。

  1. 只记问题,不记过程。 不为每一步、每个成功任务落行;请求日志已有的计数就是分母。
  2. 一条事件回答“问题在哪”,不回答“怎么复现”。 事件里是码、名字、字段路径和一句定位线索;不存计划正文、指令原文、AMIR 上下文、aira_model_read 输出。要复现时用零费用假 provider 栈按事件里的码构造,或临时提高本地日志级别。
  3. 写进已有的地方。 用现有 structuredLog 写 Workers Logs(已启用,head_sampling_rate: 1,最长保留 7 天)。“立刻修”用不到 7 天以外的数据;需要更久时再决定落 DO 表,那是另一份证据触发的另一次决定。
  4. 不是权威。 探测与写入失败一律吞掉,不改响应、不阻塞流——与 observe 现状一致。

一次请求可产生 0 到少数几条;每条最多 9 个字段:

字段 值
event agent.problem
taskId, requestId 任务与本次请求
model resolvedModel
step 出问题的模型调用步,从 1 起计(与请求记录的 steps 同一口径);来自续传裁决时为 0
kind §3.2 五种之一
code 诊断码或错误类名
operation 问题所指的原生操作或工具:plan-rejected 取 diagnostic.operation,区分编译器拒(aira_compile_exact_plan:计划本身无效或没有步骤能对应)、编译器在某一步拒(该步的 kind,如 update-imported-product:对应不完整、歧义、身份冲突等 EXACT_* 拒绝,SDK 从 ExactCadPlanCompileError 取步骤种类)与原生操作拒(model.preview 等);invalid-input 与 library-miss 取工具名
detail ≤200 字符的定位线索,见 §3.2;没有就省略
kind 判定 code detail 通常修什么
plan-rejected 续传最后一条 assistant 消息里、最后一个 step-start 之后的计划工具部件 output.status = needs-repair(更早的部件上一次请求已报过;output-error 是上一次请求已报的 invalid-input,不再报) diagnostic.code,缺失时 NEEDS_REPAIR diagnostic.message 头 200 字符 编译器拒 → 契约与宿主求值;原生操作拒 → 该操作的诊断、守恒检查、名字卡落点说明
invalid-input 本步某个工具调用 invalid: true 且错误为 InvalidToolInputError 计划工具 EXACT_CAD_PLAN_INVALID,其他工具 TOOL_INPUT_INVALID 计划工具:公开契约校验给出的第一条诊断(字段路径与 schema 用语,不含值);其他工具省略,因为校验器的消息可能引用参数值 计划工具 schema 说明、该步骤的示例
unknown-tool 本步某个工具调用 invalid: true 且错误为 NoSuchToolError NO_SUCH_TOOL 编造的工具名 工具描述;某模型反复出现则调整 allowlist
no-tool-stop 本请求没有发出任何有效的浏览器执行工具调用(计划或读模型;查库不算)、最后一步 finishReason = stop,且入站历史里没有 status = completed 的计划裁决;step 为本请求步数 PREMATURE_COMPLETION(与浏览器同名) 省略 系统提示与工具描述
library-miss 查库结果 redirected 非空;或未命中计划步骤种类且 entries 为空 LIBRARY_NO_MATCH / LIBRARY_REDIRECTED 查库词(模型撰写,可能带用户起的名字) 名字卡覆盖面、索引别名

预算截停、provider 错误和流中取消不另发事件:请求日志已有 errorCode,只需补 §3.3 的 stopReason。

gateway.request.completed 增加 taskId、steps(本请求的模型调用步数)、stopReason (step-cap / request-token-cap / output-cap,把三条 stopWhen 各包一层记下谁先命中;没有封顶时省略。SDK 只在循环本来会继续时才评估 stopWhen,所以命中的那条就是实际结束本次请求的封顶)。 有了 taskId,按任务归组、算分母、和 agent.problem 对上,都在同一份日志里完成。

指令原文、计划 JSON、currentModel、任何 aira_model_read 输出、provider 原始响应、邀请 token、原始 IP。 detail 是唯一的自由文本字段,来源限定为宿主诊断 message、schema 字段路径、工具名、查库词四种,且截断。

  • service-contracts.ts:AgentProblem、AgentStopReason 与 AgentExecutionContext.observe(priorPlanCompleted、onProblem、onStop);boundedProblemDetail 是自由文本进日志的唯一通道(折行为一行、截到 200 字符)。
  • deepseek-agent.ts:onStepEnd 在现有累计逻辑旁分类 invalid 工具调用、检查查库结果、记录是否发出过浏览器工具调用;三条 stopWhen 各包一层报告封顶原因;流结束时判定 no-tool-stop。onError 与 invalid-input 共用同一个公开契约诊断函数。观察回调抛错一律吞掉。
  • http-handler.ts:/api/agent 分支在 validateAgentChat 之后扫描入站消息:最新 assistant 消息最后一个 step-start 之后的 needs-repair 计划部件成为 plan-rejected,历史里任何 completed 计划部件置 priorPlanCompleted;消息部件按不可信输入逐字段检查。问题与封顶原因攒在请求 trace 上,随既有的 observe 一次交给 GatewayHttpObserver。
  • http/gateway-log.ts:共享的投影——一条请求记录加每个问题一条 agent.problem(带 requestId、taskId、model)。Worker 用 console.log(object) 交给 Workers Logs;Node 宿主 server.ts 把同一对象写成 stdout 的一行 JSON。

Workers Logs 的查询界面按 event = agent.problem 过滤,再按 kind、code、model 分组;按 taskId 能把同一任务的 请求记录与问题事件串起来看。7 天内按 requestId 排障与现有做法相同。不写脚本。

每次发布后看一次分组计数:哪一类、哪个码在当前版本最多,按 §3.2 最后一列去修产品代码或工具描述,不用 prompt 补丁掩盖; 同一个码按 model 分布明显不均时,调整模型策略。修完后下一版本看该码是否下降;没降再决定是否付费重跑对应的基准格。

不新建测试文件;用现有 build / typecheck / lint 加实际运行验收。

阶段 内容 验收
P0 本地(已完成,2026-09-14) 观察契约、五种判定、续传裁决扫描、请求日志三字段、共享日志投影、Node 宿主输出 已用脚本化的假 DeepSeek fetch 直接驱动构建后的 handler(进程内,无端口、无付费)跑 9 个场景:宿主拒绝后文本放弃(两条事件)、更早步的拒绝不重复、计划 schema 无效再修好、编造工具名、查库未命中与重定向、两步封顶、成功计划、完成后的总结,以及禁止内容检查(指令原文、计划字段、Revision 哈希都不出现在任何日志行)。全部通过;typecheck、lint、build 与 Worker dry-run 打包通过。未跑浏览器整栈
P1 云端 无新增改动,随下一次已授权的网关部署上线 部署后 Workers Logs 能按 event = agent.problem 过滤
  • 7 天保留不够用(想看跨版本趋势)→ 再决定把 agent.problem 落到安全状态 DO 的一张表。
  • 事件能定位类别但复现不了 → 再决定保留失败计划正文,且以邀请条款告知为前提。
  • 请求日志里成功计划数远少于任务数(说明大量任务在两次请求之间被取消)→ 再决定让浏览器补一条终态。
  • 量级让单一 DO 或日志成本成问题 → 再考虑 Analytics Engine。
关注点 位置
问题与观察契约 apps/server/src/service-contracts.ts(AgentProblem、AgentObservation、boundedProblemDetail)
判定与封顶报告 apps/server/src/model/deepseek-agent.ts(onStepEnd、reportedStop、reportTextOnlyStop)
续传裁决扫描、请求记录字段 apps/server/src/http/http-handler.ts(carriedPlanJudgements、GatewayHttpObservation)
日志投影 apps/server/src/http/gateway-log.ts(structuredObserver)
宿主写入 apps/server/worker/production-worker.ts(structuredLog)、apps/server/src/server.ts(writeJsonLine)
指标定义与阈值 生产运行条件 §4(只加数据来源引用)