跳到主内容
创建时间:最后更新:作者:白川

排查模型请求与上下文问题

对比历史、转换后的消息和请求边界,用脱敏摘要排查模型看不到信息与响应异常。

排查模型请求与上下文问题

模型没有使用刚刚读取的文件,不一定是推理能力不足。文件内容可能只显示在 UI,工具结果可能没回填,也可能在构建请求时被过滤。排查应先确认信息跨过了哪些边界。

沿着“原始会话 → 上下文转换 → 模型消息 → Provider 请求 → 响应解析”逐段比较,比把所有历史打印出来更容易找到差异。

本系列的独立示例位于仓库 columns/coding-agent/examples/debugging-observability/,固定使用 pi-agent-core、pi-ai、pi-schema、pi-telemetry 1.5.0。先在该目录执行 npm install,下文命令也从该目录运行。示例不需要模型密钥,不读取个人配置。

在实际请求入口观察

示例 src/mock-agent.ts 的 streamFn 接收 Agent 转换后的模型上下文。它在这里记录摘要,而不是从界面的聊天列表推测请求内容。运行 npm run demo 后,第一次请求只有系统和用户消息,第二次增加助手工具调用和工具结果。

摘要函数完整实现如下:

ts
import { object } from "./recorder.js"

// 只记录形状与大小,不复制消息正文、参数、URL、请求头或凭据。
export function requestSummary(raw: unknown) {
  const context = object(raw)
  const messages = Array.isArray(context.messages) ? context.messages : []
  const roles: Record<string, number> = {}
  let textChars = 0
  let toolCalls = 0
  let toolResults = 0
  for (const rawMessage of messages) {
    const message = object(rawMessage)
    const role = ["system", "user", "assistant", "toolResult"].includes(String(message.role))
      ? String(message.role)
      : "other"
    roles[role] = (roles[role] ?? 0) + 1
    if (role === "toolResult") {
      toolResults += 1
    }
    if (typeof message.content === "string") {
      textChars += message.content.length
    } else if (Array.isArray(message.content)) {
      for (const rawBlock of message.content) {
        const block = object(rawBlock)
        if (typeof block.text === "string") {
          textChars += block.text.length
        }
        if (block.type === "toolCall") {
          toolCalls += 1
        }
      }
    }
  }
  return { messageCount: messages.length, roles, textChars, toolCalls, toolResults }
}

这里的 textChars 是 JavaScript 字符串长度,既不是 UTF-8 字节数,也不是 Token 数。工具参数和图片等内容未纳入文本计数;它只作为同一采集口径下的差异线索。

摘要保留角色数量与数据形状,不携带正文。两个摘要相同不能证明内容完全相同;需要比较内容时,应在受控本地会话开启短期捕获,并对比具体片段,不能把摘要当成完整内容哈希。

区分不同层的调试入口

入口可以观察什么使用前需要核对
transformContext / convertToLlm
转换前后的消息
不能为了调试破坏返回值或抛错中断循环
模型 streamFn
交给流实现的模型上下文
不等同最终 HTTP payload
onPayload
Provider 发送前的 payload
返回值可能替换请求;纯观察时返回 undefined
onResponse
HTTP 状态与响应头
不代表流已经读取成功,也不覆盖所有传输
onProviderStreamEvent 或宿主原始事件
Provider 流数据
可能包含敏感内容,需显式开关和容量限制

官方 debug-provider 扩展通过原始流事件捕获并追加自定义会话条目,适合本地定位解析差异。它的职责不是生产日志系统:开启前应明确保存位置、读取者、事件上限与清除方式。也不要默认把鉴权头和完整工具输入上传到远程后端。

从症状选择第一项证据

模型找不到上一轮结果,先检查请求中是否有对应 toolResult;工具未被选择,先看当前声明集合和工具描述;请求立即被拒绝,再看端点、鉴权、状态及参数协议;文本到一半消失,则要区分流中断、解析失败和 UI 投影失败。

会话文件记录历史,模型请求可能经过裁剪、压缩和转换。SessionManager 的当前分支与最终上下文构建比随意赋值 agent.state.messages 更接近宿主实际行为。为了排障直接改内存消息,可能暂时改变显示,却没有修改持久化上下文。

不把“可观察”变成“自动追踪”

本地快照的请求选项有 telemetryContext,但它的存在不意味着每个 Provider 自动产生完整 span。README 提及的部分 Agent 遥测 Schema 导出,在该快照源码中没有找到。示例采用实际存在的接口,不把文档中的未来集成当作运行事实。

先用不含凭据的固定夹具建立预期摘要,再接入某个真实 Provider 验证回调是否触发、状态是否能取到、取消如何返回。没有这一步,只能说观察函数可用,不能声称真实模型请求已经调通。

依据与版本

本地官方快照为 8efd314fa,相关包标记 1.3.0;配套代码另按已发布的 1.5.0 类型和运行行为核验。主分支文档持续变化,不能混作同一版本。