Skip to content

🐣 小a上岗后状况百出

小a造的 Agent 跑起来了,但状况不断——模型变笨了、缓存没命中、上下文爆了、Agent 卡死循环……

老z掏出一本《事故录》:"前人都替你踩过坑了,翻翻。 每个问题我给你:现象(用户怎么骂)→ 根因(拆解)→ pi 怎么应对 → 你怎么自查。"


E.1 模型为什么"降智"了?★重点

现象:同一个模型,在 Agent 里明显不如官网聪明——不思考了、该调工具不调、看不懂图了。

根因(三条,都和思考强度/能力开关有关):

  1. 能力开关没开:模型有个"会不会思考"的属性。配置里没开 → pi 不知道模型会思考 → 调用时不发 thinking 参数 → 模型没被要求思考 → 显得笨。
  2. 思考强度映射错:各厂商思考参数格式不同(pi 有 10 种 thinkingFormat,第 2 章)。映射错了 → 模型想了但没想够。
  3. 被配置覆盖:override 机制把强模型能力改错了。

pi 怎么查:model-resolver.ts 的解析链 + provider-attribution.ts 归因。

自查清单:

  • [ ] 模型的 reasoning 字段是 true 吗?
  • [ ] thinkingLevel 映射对了吗?(对照厂商文档)
  • [ ] 有没有被 override 覆盖?(检查 models.json)

E.2 Prompt 缓存为什么没命中?★重点

现象:同样的对话,token 费用没降;状态栏显示"cache miss"。

缓存原理:像图书馆借书——上次的"书"(prompt)5 分钟内还在架上,再借就是"缓存读"(便宜很多);超时或内容变了,得重新"录入"(贵)。

没命中的五个根因:

  1. 空闲超 5 分钟:CACHE_TTL_MS = 5 * 60 * 1000(pi 源码常量),超了缓存过期。头号原因
  2. prompt 前缀变了:缓存按前缀匹配,系统提示词/历史改一个字,前缀就断。
  3. 换模型了:不同模型缓存隔离。
  4. 厂商不支持:有些 provider 不报缓存(pi 能识别这种"沉默")。
  5. 低于噪声线:NOISE_FLOOR_TOKENS = 1024,太小的不算。

pi 怎么算:cache-stats.tsdetectMiss() 算出 missedTokens / missedCost(白花了多少钱)。

自查清单:

  • [ ] 状态栏看 cache 读占比
  • [ ] 别频繁改系统提示词
  • [ ] 长会话别停太久(超 5 分钟)

省钱倍数:Anthropic 缓存读约便宜 10 倍;OpenAI 约 2 倍(因厂商而异)。


E.3 输出为什么被截断 / 工具结果丢失?

现象:工具输出只剩一半;bash 输出几千行 Agent 就懵了。

根因:truncate.ts 截断 + output-accumulator.ts 累积 + output-guard.ts 守卫。

pi 的做法:分级截断(头尾保留、中间省略)。

自查:

  • [ ] 工具输出超过 5000 字符?会被截断
  • [ ] 看截断后有没有"...(省略)..."标记

E.4 上下文为什么会爆炸 / 怎么救?

现象:聊着聊着报 "context length exceeded"。

根因:窗口塞满(第 1 章伏笔)。

pi 的救法:compaction/branch-summarization.ts——分支总结,不是粗暴截断。

自救(我们的 Agent):第 28 章的滑动窗口——留首尾+摘要。

自查:

  • [ ] history 是不是太长了?
  • [ ] 大文件被 read 进上下文了?

E.5 为什么 Agent 卡住 / 死循环?

现象:同一个工具调来调去;或者转圈不出结果。

根因:循环终止条件没触发、tool 调用上限、流式中断。

pi 的机制:agent-loop.ts 的迭代次数限制 + abort 机制 + shouldStopAfterTurn hook。

自救:第 26 章的 maxTurns——设个上限(如 20),超了强制停。

自查:

  • [ ] maxTurns 设了吗?
  • [ ] 模型是不是反复调同一工具?(可能是工具描述不清,它不知道用别的)

E.6 为什么鉴权失败 / token 刷新挂了?

现象:突然 401;refresh token 失效。

根因:auth/resolve.ts 责任链走到哪断了;OAuth token 过期没续。

pi 的设计:无静默降级——失败就显式报错,不偷偷退回环境变量(fail loud)。

自查:

  • [ ] API key 对吗?环境变量设了吗?
  • [ ] OAuth token 过期了吗?

E.7 并发写文件为什么会坏?

现象:两个工具同时写一个文件,内容损坏。

根因:并发竞态。

pi 的解法:file-mutation-queue.ts——同文件串行,不同文件并发(第 20 章 + 附录 D.9 命令模式)。

自救:我们的 Agent 串行调工具(第 26 章 for 循环),暂时没这问题。但如果将来加并发,必须加队列


E.8 远程会话为什么会"状态不一致"?

现象:客户端显示的和服务器实际的对不上。

根因:snapshot(权威)和 progress(瞬态)混用。

pi 的铁律:snapshot 是唯一真相源,progress 只是 UI 提示(第 21 章)。

关键认知:这不是 bug,是设计取舍——progress 可能丢/乱序,但 snapshot 保证最终一致。别用 progress 累积状态,必须以 snapshot 为准重建。


E.9 触发限流(429)怎么办?★新增

现象:调用太频繁,厂商返回 429 Too Many Requests。

根因:超过厂商的速率限制(按分钟/按天)。

应对:退避重试(exponential backoff)

typescript
async function withRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try { return await fn(); }
    catch (e) {
      if (e.status !== 429) throw e;
      const wait = Math.pow(2, i) * 1000;  // 1s, 2s, 4s 指数退避
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw new Error("重试次数耗尽");
}

为什么指数退避:一开始等短(可能马上就好),越来越长(给厂商喘息)。比固定等待更有效。

自查:

  • [ ] 是不是循环里调太密了?
  • [ ] 大批量任务要不要加并发限制?

排查通用心法

🧙 老z的排查三步:

  1. 看现象:用户怎么骂的?错误信息是什么?
  2. 找根因:别只治症状。pi 源码的注释常写明"为什么这么设计"
  3. 改对地方:别在症状层打补丁,要在根因层修

核心原则:fail loud。pi 的设计哲学是"出问题就显式报错",不静默吞掉。你看到错误,比错误被藏起来好。


事故速查表

症状可能原因看哪节
模型变笨思考能力没开/映射错E.1, 第 2 章
费用没降缓存没命中E.2
输出不全被截断E.3, 第 20 章
context exceeded窗口爆了E.4, 第 28 章
Agent 卡死死循环E.5, 第 26 章
401 错误鉴权失败E.6
文件损坏并发写E.7
远程状态不对snapshot/progress 混用E.8, 第 21 章
429 限流调太密E.9

参考

  • 源码:cache-stats.ts(CACHE_TTL_MS, NOISE_FLOOR_TOKENS)、auth/resolve.tstruncate.tsfile-mutation-queue.ts
  • 对应章节:第 2/20/21/26/28 章