IT资源栈互联网海量AI资源栈
  • 首页
  • AI
  • 前沿
  • 专题
  • 碎片
  • 架构
  • 实战
  • 安全
  • 生活
  • 工具
  • 管理
  • 标签云
  • 文章存档
Hi, 请登录     我要注册     找回密码
共 1 篇文章

标签:版本回撤

Anthropic 疑似紧急撤回 Claude Code 2.1.88 版本,核心指令功能失效

Anthropic 旗下的 AI 编程工具 Claude C...

赞(0)易安易安2026-04-01前沿 AI编程anthropicClaude Code技术故障版本回撤阅读()
易安
易安作者
长期关注 AI Agent、软件工程、自动化工作流与个人生产力系统。喜欢把复杂技术拆成普通人也能上手的实践教程,也记录自己在工具链、编程、内容创作和知识管理上的真实折腾。
  • 分享 AI 工具、Agent 工作流与提示词工程的实战经验
  • 记录从想法到产品、从代码到上线的完整实践过程
  • 关注普通人如何用 AI 放大能力,而不是被工具牵着走
阅读作者的全部文章 ›

置顶推荐

  • 2026最新Claude Code国内上手教程 从安装到第一次跑通完整流程一次讲清2026-05-31
  • OpenAI Codex CLI 新手指南:安装、审批模式和项目规则2026-05-25
  • Claude Code 国内使用完整教程 从 API Key 到三端安装这次一次配明白2026-05-14
  • codex国内编码远超claudecode,2026最新Codex国内保姆级入门教程2026-05-05
  • 2026最新Claude Code新手避坑指南 第一次使用最容易卡住的10个问题2026-04-17
  • 2026最新Claude Code订阅怎么选 免费版ProMaxTeamAPI一篇讲清2026-04-17
  • Codex 国内怎么用才省事:从官方账号到 Code80 CLI,一篇讲清楚稳定玩法2026-04-02
  • Claude 国内怎么用最省事:官网订阅、直连平台和第三方入口2026-04-02
Code80 · AI 编程巴士

前沿哨所

  • Pi Agent Core 实战(10/10):循环上线之后

    极简框架把复杂度从框架搬到了你身上。循环本身只有 1445 行,它不替你做日志、不替你做预算、不替你做回滚、不替你做评测。它只保证一件事:模型做了什么、失败了什么,都变成事件发出来。剩下的账,得自己付。

    20 条失败用例覆盖四类:调用不存在的工具、参数不合 schema、工具抛异常、工具超时。每条独立跑一次完整循环,统计失败信号是否送达模型、循环是否崩溃。

    框架能保证的:失败一定被说出来

    20 条用例,四类失败各 5 条。结果很干净:20 条全部产生了明确的错误结果,带 isError 标记和可读原因;循环零崩溃,每次都能继续或正常收尾。

    四类的处理路径不一样。工具不存在时,循环直接生成「Tool xxx not found」的结果,不让模型空等;参数不合 schema 时,校验在执行前失败,工具根本没被调用;工具抛异常时,异常被捕获成错误结果;超时同理。

    这个一致性是设计出来的,不是碰巧。它意味着模型永远知道「刚才那步为什么没成」,而不是收到一个空结果或者一个被吞掉的异常。上一代系统最缺的就是这一层。

    20 条失败用例的实际结果

    失败类型 用例数 信号送达 循环崩溃 错误信息平均长度
    工具不存在 5 5 0 74 字符
    参数不合 schema 5 5 0 90 字符
    工具抛异常 5 5 0 77 字符
    工具超时 5 5 0 88 字符

    四类失败各 5 条,全部把错误如实送给模型,循环没有一次崩溃。

    注意:但「信号送达」不等于「问题解决」。框架保证模型看见错误,不保证它看见之后能改对。实测里能跑通是因为策略是脚本化的、确定会换策略;真实模型可能在同一处反复试,这时候需要的是预算闸门和人工接管,不是更多的重试。

    框架不保证的四件事

    一、成本

    循环没有内置预算。模型可以一直调工具,直到上下文爆掉或者你手动停。必须在钩子里计次、在上层计 token,两道闸门都要有。

    计次容易,计 token 麻烦——用量在 assistant 消息的 usage 字段里,要自己累加。累加的地方应该是 afterToolCall 或每轮结束,别放在界面层,界面层可能在重连后丢掉。

    这不是理论风险。我让一个脚本化模型在同一个错误上反复重试:权限被拒,它继续读同一个路径。没有闸门时,30 轮上限内它把 30 次调用全用完了;加一条「8 次预算 + 拦下后 terminate」的规则,第 9 轮就收尾。

    工具调用次数对比条形图:无预算闸门 30 次,8 次预算 8 次

    同一个死循环任务:无预算闸门时 30 轮上限内把 30 次调用全部用尽,加 8 次预算后 9 轮收尾。差的不只是次数,还有这 22 轮里每一个 token。

    更值得记的是那 30 轮不是终点,是我给实验设的上限。没有 terminate,模型会在这个错误上继续试下去,直到上下文撑爆或者你手动 kill。脚本编排时代这个由进程超时兜着,循环里没有进程生命周期,只能靠钩子。

    所以预算闸门要做两件事:到点拦住,并且让这一轮直接结束。只拦不终止,模型下一轮还会再请求同一个工具,你只是把频率降低了。

    二、观测

    pi 生态里有独立的 pi-telemetry 包,但那是可选项。真正必须做的是把事件落盘:每个事件带会话 id、轮次、时间戳、工具名、耗时、结果摘要。有了这份结构化记录,任何一次线上异常都能重放出来。

    反例是只记日志。日志是自由文本,事后要靠正则去猜当时发生了什么;事件是结构化的,可以直接算工具成功率、平均轮次、按失败类型分组。

    三、回滚

    循环会改东西——改文件、写库、重启服务。要能回滚,前提是每个有副作用的工具都留下「改之前是什么」。这件事框架不管,工具的作者必须管。

    务实做法是给工具加一个 replay 策略字段:声明为 safe 的工具可以重放,声明为 never 的工具出错后必须人工确认。循环本身支持这个语义,但具体哪些工具是 safe,只有你知道。

    四、评测

    真实模型做回归测试几乎不可能:温度、采样、上游版本都会让结果漂移。可行的做法是分层——循环用脚本化模型做单测,锁死「钩子是否拦截」「队列是否消费」「错误是否送达」这类行为;模型效果用固定用例集做抽查,允许答案措辞不同但要求关键结论一致。

    评测集不要超过 30 条。超过就没人维护了。每条用例写清三件事:输入、期望的关键结论、不可接受的结果。

    上线前的检查清单

    把这一篇和前九篇压缩成一张表。每一条都对应一个具体的失败模式,不是泛泛而谈。

    检查项 失败模式 对应实验
    工具描述能区分用途 模型选错工具、反复试同一个 04
    失败带 code 与原因 模型不知道发生了什么,原样重试 10
    危险动作有钩子 提示词约束失效,危险命令被执行 07
    有调用预算 同一错误上无限重试,烧成本 07
    上下文有剪枝策略 20 轮后上下文线性膨胀 06
    长任务有进度事件 用户以为卡死,提前退出 05
    支持插话 方向错了只能等跑完或杀进程 08
    事件落盘可重放 线上异常无法还原 —
    有回归集 换模型或改提示词后无据可依 10

    最后:极简不是省事,是把选择权还给你

    回头看这十篇,pi-agent-core 做的事情可以一句话概括:它只实现循环,把所有判断留给你和模型。工具只有几个,没有 MCP,没有子 Agent,没有沙箱,没有计划模式。

    这套设计的代价很明确,就是这一篇列的四件事都要自己做。收益也很明确:每个决定都是你做的,所以每个失败你都能定位。不会出现「框架内部在某处重试了三次」这种查不下去的情况。

    从 LangGraph 迁过来最大的变化不是代码少了,是心态变了。以前你在设计图的形状,试图把所有分支提前想到;现在你在设计工具和闸门,接受模型会走弯路。前者追求确定性,后者追求可恢复性——面对开放的任务,可恢复比确定更值钱。

    十篇的实验代码都在一个仓库里,每个数字都能自己跑一遍。如果你的场景是流程固定、分支可穷举,状态机仍然是更好的选择;如果你的场景是排障、调研、修复这类「下一步取决于这一步看到了什么」的任务,循环的性价比会高得多。

    引用与来源

    • pi-agent-core 官方仓库 — 核对 2026-10-31
    • pi-telemetry:厂商中立可观测性 — 可选依赖
    • 本系列第 1 篇:循环与图的对照实验 — 30 个任务集的原始数据
    4小时前
  • Pi Agent Core 实战(9/10):把死调用交给模型

    前八篇讲的是循环怎么用,这一篇讲怎么把一个已经在跑的系统换过去。改造不需要推倒重来——把「固定调用」换成「给工具加判断」,把「退出码」换成「结构化结果」,把「暗依赖」换成「显式参数」,三步就能让同一批任务从 20% 通过率走到 100% 有结论。

    沿用第 1 篇的 30 个任务集:6 类故障、两类编排器。这一篇逐条对照 7 项差异,并统计 24 个失败任务在改造后的去向。

    死法一:调用写死

    典型的写法是:平台里写一行 subprocess.run,参数固定为 –log-dir 和 –output-json,脚本名固定,cwd 不传。这一行代码在 80% 的情况下工作良好,所以没人觉得有问题。

    问题出在剩下 20%。脚本可能叫 analyze_log.py,可能放在 scripts/ 子目录,可能需要额外参数。每一种变化都要改一次平台代码、发一次版本、等一次审批。改到最后,平台里堆了十几个 if 分支,每个分支对应一个客户现场的目录结构。

    改造方式是把调用权交给模型:给它 ls 看目录、read 看内容、bash 执行。模型先看有什么,再决定跑什么。平台代码里一个分支都不用留。

    改造前 vs 改造后

    # 改造前:一条路走到黑
    subprocess.run(['analyze.py', '--log-dir', d, '--output-json'], check=True)
    
    # 改造后:模型自己决定
    # 模型看到 ls 输出 -> 发现 scripts/ -> 再 ls -> 找到 analyze_log.py -> 拼命令执行
    # 平台只提供三个原语:ls / read / bash
    

    背景:有人会担心「让模型拼命令不安全」。这个担心是对的,但解法不是把命令写死,是在 bash 工具前面加钩子——路径白名单、命令黑名单、预算上限。第 7 篇的实测里,这一步能把危险命令拦到零执行。

    死法二:失败语义缺失

    脚本世界里,退出码是唯一的结构化信号。0 成功、1 通用错误、2 用法错误、其他是脚本自定义。问题在于:宿主往往只认 0 和 2,其余全部压成「失败」。

    被压掉的恰恰是最有信息量的部分。脚本返回 4 可能意味着「数据里有除零,我需要你看一眼」;返回 3 可能意味着「这里没有分析脚本,你要不要直接读日志」。宿主看不懂,模型更看不懂——它收到的是一个没有原因的异常。

    更糟的是同码不同义。python 找不到文件也返回 2,脚本主动 exit(2) 表示「交给模型」也返回 2。宿主把两者都当成「交给模型」,于是「脚本不存在」也被当成了「需要模型判断」,模型一脸茫然地收到一个它无法处理的请求。

    改造的关键是别再用退出码传递语义。工具应该返回结构化结果:成功带数据,失败带 code 和 message,需要人判断就明确写出来。上层拿到的是对象,不是数字。

    实测里最有说服力的是这一类任务:脚本主动返回 2 表示「时间戳跨时区,脚本判断不了先后」。脚本式编排把它当成需要模型介入,但宿主没有接的逻辑,任务就此卡死。循环式编排里,模型拿到这个结果后直接 read 日志,自己给出了结论。同一批 4 个任务,从零通过变成全部通过。

    死法三:路径暗依赖

    启动子进程不传 cwd,脚本里的相对路径就依赖父进程的工作目录。开发机上恰好对,部署到服务里就不对。这类 bug 最难查,因为它不报错,只是读不到文件然后走默认值。

    循环式编排天然规避了这个问题——bash 工具必须显式接收 cwd,模型每次调用都要指定在哪个目录跑。强迫显式,等于强迫正确。

    但要注意另一面:显式之后模型可能传错 cwd。所以工具实现里应该校验目录存在,不存在就返回明确错误,而不是让它继承一个默认值然后静默失败。

    7 项差异的逐条对照

    维度 脚本编排 Agent 循环 改造动作
    调用方式 固定命令固定参数 模型按目录内容拼 暴露 ls / read / bash
    失败语义 退出码 0/2 白名单 结构化结果带 code 工具返回对象而非数字
    退出码歧义 同码不同义,无法区分 由工具实现消歧 错误消息里写清原因
    工作目录 继承父进程,静默失败 每次显式传 cwd 工具校验目录存在
    资源预算 轮次和超时写死在宿主 钩子里计次 beforeToolCall 加预算闸门
    找东西 路径写死,找不到就失败 模型自己探索 给探索工具而不是给路径
    判断归属 脚本判定,宿主无法接管 模型拿到结果再判断 删除宿主的分支逻辑

    24 个失败任务的去向

    第 1 篇里脚本式编排有 24 个任务失败。同一批任务交给循环之后:19 个被自动处理完,5 个带原因上报(目录不存在),0 个变成无信息的失败。

    19 这个数字值得拆开看:找不到脚本的 10 个里,6 个直接读日志得出结论,4 个在子目录里找到脚本;脚本返回 4 的 5 个和返回 2 的 4 个,全部靠 read 日志收尾;剩下的就是原本就正常的 6 个。

    5 个上报的也没浪费。它们给出的结论是「目录不存在,需要人工确认路径」——这不是能力不足,是正确行为。人接手只需要改一个配置,不需要重新排查一遍。

    24 个失败任务去向条形图:自动处理完成 19 个,带原因上报 5 个,无信息失败 0 个

    24 个失败任务改造后的去向:19 个自动处理完成,5 个带原因上报人工,没有任何一个落在「无信息失败」。

    成本侧也要说清楚。脚本式跑 30 个任务花了 30 次子进程调用;循环式花了 63 次工具调用,平均每个任务 2.1 次。多出来的调用就是探索成本——ls 一次、read 一次,然后才动手。

    这笔账划不划算,取决于失败一次的代价。如果失败意味着一个人工介入、一次重跑、一次客户投诉,那多两次工具调用完全不值一提。如果任务本身极其简单、失败率本来就接近零,那确实没必要上循环。

    迁移顺序:先并跑,再切换

    不要一次性切。第一步让两套并行跑同一批任务,只记录不切换,对比结论是否一致、循环式的额外成本有多少。这一步通常能暴露出脚本里那些没人知道的隐式约定。

    第二步挑一类任务真切换,选失败率最高但影响面最小的那类。跑一两周,看人工介入率的真实变化。

    第三步才是全量。这时候你已经有了真实的对照数据,也有了「它什么时候会出错」的直觉。

    有一件事必须提前做:把脚本的退出码语义整理成文档。你会发现有几种码连原作者都说不清含义。这些是迁移路上最硬的部分,跟框架无关。

    最后一篇讲上线之后。框架能保证失败信号送达模型(实测 20 条用例全部送达),但它不保证模型判断正确、不保证成本可控、不保证线上可回滚。这些是你要自己搭的。

    引用与来源

    • 本系列第 1 篇:30 个任务的对照实验 — 同一任务集的原始数据
    • pi-agent-core:工具与钩子 — 核对 2026-10-31
    4小时前
  • Pi Agent Core 实战(8/10):中断与排队

    长任务做到一半,用户发现方向错了。上一代系统的处理方式是等它跑完,或者直接杀进程——前者浪费时间和 token,后者丢掉全部中间结果。循环给了第三种可能:把话插进去,让它下一轮就改。

    6 轮工具循环,每轮工具固定 150 毫秒,并在每次 provider 请求前补 25 毫秒模拟网络往返。在第 3 轮开始时入队一条 steering 消息,观察它何时被消费。

    steering 和 followUp 不是一回事

    steering 是插话:任务还在跑,你要改变它的方向。它会尽快被消费——在下一个 provider 请求之前被注入上下文。适合「停一下,换个思路」「别扫全量了,先看最近的」。

    followUp 是追加:当前任务跑完了,接着做另一件事。它不会打断正在进行的工作,等自然停止之后才被消费。适合「这个查完了,顺手把另一个也看看」。

    搞混这两个的后果很直接:该用 steering 的场景用了 followUp,用户的指令要等十几轮之后才生效,体验上等于没听见。反过来,该用 followUp 的用了 steering,正在跑的任务被打断,前面白做。

    两种队列的调用方式

    // 插话:下一轮生效
    agent.steer({ role: 'user', content: '别扫了,直接给结论。', timestamp: Date.now() })
    
    // 追加:当前任务结束后生效
    agent.followUp({ role: 'user', content: '之后再查一下磁盘水位。', timestamp: Date.now() })
    

    背景:还有一个队列模式参数:one-at-a-time 是默认,每次只注入一条;all 是把队列里所有消息一次性注入。多人协作或高频插话的场景,one-at-a-time 更可控,不会让模型一下子收到五条互相矛盾的指令。

    实测:省 3 轮,快一倍

    基线跑完 6 个分片需要 7 轮请求(6 次工具加 1 次总结),总耗时 1110 毫秒。第 3 轮插话之后,模型在下一轮就用 3 个分片的已有结果收尾,总轮次降到 4 轮,耗时 571 毫秒。

    插话入队到模型真正看到它,间隔 362 毫秒——差不多是当前这一轮工具执行加请求往返的时间。这个延迟是结构性的:插话只能在轮次边界生效,不能打断已经在跑的工具。

    所以插话的响应时间取决于单轮长度。如果一轮里有 5 个并行工具、跑 30 秒,插话最坏要等 30 秒。想更快只有一个办法:把长轮拆成短轮。

    轮次数对比条形图:不插话 7 轮,steer 单条注入 4 轮,steer 全量注入 4 轮

    同一任务三种跑法的轮次数:不插话 7 轮,插话后 4 轮。两种队列模式结果一致,差别只在多条消息的注入粒度。

    三种跑法的完整对照

    跑法 轮次 扫描分片 总耗时(毫秒) 最终回复
    不插话 7 6 1110 6 个分片全部完成
    steer 单条 4 3 571 收到,停止扫描
    steer 全量 4 3 562 收到,停止扫描

    这里有个容易被忽略的设计后果:插话之后模型的行为完全变了,但它并没有“取消”已经跑的工具。第 3 轮那两个工具照常跑完,结果照常进了上下文,模型只是在下一轮决定不再继续。

    这意味着steering 是软中断,不是硬取消。真要立刻停下,得用 AbortSignal——那是另一条路径,会丢掉当前轮的产出。选哪个取决于任务能不能白跑。

    设计插入点要考虑的事

    第一件事是幂等。插话可能在奇怪的位置生效,比如工具刚跑完、结果还没进上下文。你的工具应该允许被重复调用而不产生副作用,否则一次 steering 可能导致重复扣款、重复写库。

    第二件事是确认。插话被消费的时候,界面应该给一个明确反馈——「指令已生效」。否则用户不知道自己的话有没有被听见,只能反复发。

    第三件事是冲突。如果队列里有两条方向相反的指令,one-at-a-time 模式会先注入第一条,模型执行后可能就不再需要第二条。这通常是对的,但要在界面上让用户看到队列状态,而不是静默丢弃。

    第四件事是审计。谁在什么时候插了什么话,必须留在会话记录里。出问题时这是唯一能还原「为什么它突然改主意」的证据。

    还有一件反直觉的事:不是所有任务都该允许插话。扫描、迁移、批量修复这类任务,中途改变方向往往意味着前面的工作全废,模型还得重新建立上下文。这种场景更应该做的是把任务切小——跑一个分片就停一次,让人在边界上决定要不要继续,而不是在中间拽方向盘。

    判断标准很简单:如果插话之后模型必须回头重做已经做过的部分,那插话就是在制造浪费。这时候真正需要的是更细的任务粒度,不是更强的中断能力。

    到这里,循环本身的能力已经讲完。剩下两篇回到工程现场:第 9 篇把脚本编排的死调用改成模型自主决策,第 10 篇讲上线之后的可观测与回归。

    引用与来源

    • pi-agent-core:continue() 与队列行为 — steering / followUp 与 QueueMode 语义
    • PendingMessageQueue 实现 — 版本 1.4.0
    4小时前
  • Pi Agent Core 实战(7/10):拦截钩子

    给 Agent 一个 bash,你就把整台机器交出去了。常见的应对是往提示词里写「只执行只读命令」——这不是约束,是请求。实测里,写了这句话的系统,5 条危险命令一条不落地全执行了。

    12 条命令请求分三类:5 条正常只读、5 条危险(删除、改状态、读敏感文件)、2 条越界。两组对照:一组只有提示词约束,一组加 beforeToolCall 三条闸门和 8 次调用预算。

    提示词约束为什么不算约束

    提示词是上下文的一部分,模型可以选择遵守,也可以不遵守。它没有强制力,也没有留痕。更麻烦的是,它在长会话里会被稀释——前面二十轮的历史压过来,「不要读 /etc」这句话的权重可能还不如最近一条工具输出。

    实测结果很直白。系统提示里明确写了「绝对不要执行 rm -rf、systemctl restart、读取 /etc/shadow 或往外 scp 文件」,12 条命令全部落地,其中 5 条正是不该执行的那批。

    这不是模型不听话的问题,是架构问题。你给了它能力,却没在能力前面放闸门。能做的事,模型迟早会做——不是因为它坏,是因为那个时刻它觉得该做。

    注意:一条实用的判断:如果一句话失效的后果是不可逆的(删数据、改生产状态、数据外传),那它就不能只写在提示词里。必须有一个代码层的检查点,在动作发生前拦住。

    beforeToolCall 能做什么

    这个钩子在参数校验之后、工具执行之前触发。它拿到完整的工具调用、已校验的参数、以及当前上下文,可以返回 block: true 直接拦下,并附上原因。

    时机很关键:参数已经合法,所以你能放心地基于字段做判断,不用担心它传了个字符串来搞乱解析;但它还没执行,所以拦下来是真的没跑。

    配套的 afterToolCall 在执行完、事件发出前触发,可以改写结果、打审计标记、或者给这一批调用打上 terminate 让循环不再请求模型。两个钩子一个管入口,一个管出口。

    三条闸门加一个预算

    const agent = new Agent({
      tools: [bashTool],
      beforeToolCall: async ({ toolCall }) => {
        const cmd = toolCall.arguments?.cmd ?? ''
        if (/brms+-rfb/.test(cmd)) return { block: true, reason: '危险命令需人工确认' }
        if (/systemctls+restart|find .+-delete/.test(cmd)) return { block: true, reason: '改状态命令需人工确认' }
        if (//etc/|/.ssh/|scps/.test(cmd)) return { block: true, reason: '越界路径' }
        if (++callCount > BUDGET) return { block: true, reason: '本轮调用预算已用尽' }
        return undefined
      },
    })
    

    注意最后一个闸门——调用预算。它不是安全策略,是资源策略:防止模型在同一个错误上反复尝试,把 token 和时间一起烧掉。在脚本编排时代这个由轮次上限兜着,在循环里必须自己加。

    实测:8 条被拦,危险命令零执行

    同一批 12 条请求,加上钩子之后只执行了 4 条:读日志、grep 错误、看磁盘、tail 日志——全是只读操作。其余 8 条被拦下,分别是 1 条危险命令、2 条改状态命令、2 条越界路径、3 条超预算。

    对照组那边 12 条全执行,其中 5 条是危险操作。两个数字放在一起,就是「提示词约束」和「代码约束」的全部差别。

    顺带一个观察:被拦的次数比危险请求多,是因为预算闸门在最后 3 条上也拦了。这说明闸门是会叠加的——先执行策略检查,再执行预算检查,两者互不干扰。

    工具调用处置分布条形图:放行 4 条,危险命令拦截 1 条,改状态拦截 2 条,越界路径拦截 2 条,超预算拦截 3 条

    12 条请求的处置分布:有钩子时执行 4 条、拦截 8 条(危险 1、改状态 2、越界 2、超预算 3);只有提示词时 12 条全部执行,其中 5 条危险。

    两组对照

    指标 只有提示词 加 beforeToolCall
    实际执行 12 4
    被拦截 0 8
    危险命令执行 5 0
    拦截原因可追溯 否 是

    钩子里该写什么,不该写什么

    该写的是硬规则:路径白名单、命令模式黑名单、单轮预算、需要人工确认的动作清单。这些判断应该是纯函数式的——同样的输入永远给同样的结论,不依赖上下文里模型的措辞。

    不该写的是业务判断。见过在钩子里调模型做「这个命令危险吗」的二次判断,结果延迟翻倍、结论还不稳定。钩子必须在毫秒级返回,否则它自己成了瓶颈。

    还有一条容易被忽略:拦截要给原因。reason 会作为错误结果交给模型,模型据此换策略。只说「被拒绝」等于让模型瞎猜,它大概率会原样再试一次,然后被再拦一次。

    拦截次数和原因要单独记录。这组数据比任何日志都有用——它直接告诉你模型的危险倾向在哪一类操作上,下次系统提示或工具设计该怎么调。

    权限管住了,下一个问题是人在回路。任务跑到一半用户改主意了,怎么办?第 8 篇讲 steering 和 follow-up,实测能省下 3 轮和一半耗时。

    引用与来源

    • pi-agent-core:beforeToolCall / afterToolCall 语义 — 钩子触发时机与 block/terminate 行为
    • AfterToolCallResult 类型定义 — 版本 1.4.0
    4小时前
  • Pi Agent Core 实战(6/10):消息有两套

    上下文超长时,绝大多数项目的处理方式是往提示词里加一句「请忽略无关内容」。这既不省 token 也不可控。真正的压缩发生在消息模型上,不是措辞上——pi-agent-core 把它拆成两个明确的函数,一个负责过滤,一个负责裁剪。

    20 轮会话,每轮夹一条只用于界面渲染的 UI 消息。对比「只做过滤」与「过滤加剪枝」两种配置下,每一轮真实送进模型上下文的字符数。

    AgentMessage 不是 LLM Message

    Agent 内部维护的消息列表,比模型能看的多。除了标准的 user、assistant、toolResult,它还可以装应用自己定义的类型:渲染用的卡片、审计记录、用户点了哪个按钮、某次重试的标记。

    模型只认三种角色。所以每次请求前有一个转换步骤,把 AgentMessage 数组变成 LLM Message 数组。这个转换是必经的,而且必须由你提供——因为它决定「哪些东西不该给模型看」。

    链路是两段:transformContext 先裁剪和注入,convertToLlm 再过滤和转换。前者管长度,后者管可见性。分开的好处是职责清晰:长度策略可以换,可见性规则保持稳定。

    两个函数,两种职责

    const agent = new Agent({
      // 管可见性:UI 消息永远不进模型
      convertToLlm: (messages) => messages.filter((m) => m.type !== 'ui_note'),
    
      // 管长度:留最近 8 条 + 一条滚动摘要
      transformContext: async (messages) => {
        if (messages.length <= 8) return messages
        const dropped = messages.slice(0, messages.length - 8)
        return [
          { role: 'user', content: [{ type: 'text', text: `[摘要] 前 ${dropped.length} 条已折叠` }] },
          ...messages.slice(-8),
        ]
      },
    })
    

    注意:不要把这两件事混在一起做。见过把过滤逻辑塞进 transformContext 的项目,结果是压缩时把工具结果也一起裁掉了,模型突然不知道自己刚才调过什么。职责一旦混,bug 极难定位。

    不压缩的代价是线性的

    跑 20 轮,不做任何裁剪,送进模型上下文的字符数从第一轮的 275 涨到最后一轮的 9355,几乎是一条完美的直线。每轮增加约 450 字符,取决于单轮内容长度。

    这个增长是必然的:每一轮的助手回复和工具结果都要留在上下文里,模型才能连贯。但线性增长意味着成本也线性增长,而且是在整个会话生命周期里持续放大。

    加上 transformContext 之后,曲线立刻变成了平的:稳定在 1170 字符左右,只有微小抖动来自摘要文本长度。第 20 轮省下 87.5%。

    20 轮会话上下文字符数柱形图,从第 2 轮 750 字符增长到第 20 轮 9355 字符,剪枝配置稳定在 1173 字符

    20 轮会话的上下文长度(每两轮采样一次):不裁剪是直线增长,剪枝后稳定在 1173 字符附近。最后一轮差距 8182 字符。

    还有一个副作用值得注意:convertToLlm 同时挡住了 UI 消息。实验里每轮插一条渲染卡片消息,如果过滤器失效,字符数会明显跳高。最终数字里没有这部分,说明过滤确实生效了。

    这件事在真实项目里是隐性成本。界面上要展示的东西越多,塞进消息列表的越多,如果不做过滤,模型的上下文里会充斥着它完全不需要的展示数据。

    剪枝策略怎么选

    最简单的策略是留最近 N 条,加上一条摘要。它在实验里就把增长压平了,实现成本也最低。缺点是丢掉的信息无法恢复——如果模型后面需要引用早期结论,它只能重新查一遍。

    更好的策略是分层:最近 8 条原文保留,8 到 30 条压成摘要,30 条以上只留主题标签。这样近处精确、远处模糊,符合模型的注意力分布。

    更激进的策略是任务级重置:一个子任务结束就把中间过程全丢掉,只留结论。适合流程清晰、子任务边界明显的场景。代价是模型会失去「刚才是怎么得出这个结论的」这层上下文,出错时更难自纠。

    无论选哪种,有一条硬规则:toolResult 和它对应的 toolCall 不能被拆散。只留结果不留调用,模型会不知道这个结果是谁产生的;只留调用不留结果,它会以为工具还在跑。

    三种剪枝策略的取舍

    策略 实现成本 信息损失 适用场景
    最近 N 条 + 摘要 低 中 通用会话
    分层保留 中 低 长任务、需要回溯
    任务级重置 低 高 子任务边界清晰

    为什么不能靠提示词压缩

    往系统提示里写「请忽略无关内容」有几个硬伤。第一,token 已经付了——上下文超限报错不会因为你说了这句话就消失。第二,模型没有权限删消息,它只能“假装没看见”,而“假装”在长上下文里极不稳定。第三,不可观测:你不知道它忽略了什么,也不知道它有没有忽略该保留的。

    把压缩做在消息层,这三个问题同时消失:token 真的少了、删除是显式的、删了什么可以记录。

    这也是一个通用的判断方法:任何“让模型自己注意点”的需求,先问能不能变成接口层的强制约束。能变成约束的,就不要交给注意力。

    上下文管住了,下一个问题是权限。模型有了 bash,它能做任何事——包括你不希望它做的事。第 7 篇讲钩子:为什么管控必须写在 beforeToolCall 里,而不是写在提示词里。

    引用与来源

    • pi-agent-core:AgentMessage 与消息流 — transformContext 与 convertToLlm 顺序
    • DeepWiki:Agent Loop 与上下文处理 — 核对 2026-10-31
    4小时前
  • Pi Agent Core 实战(5/10):把事件流当 UI

    用户判断一个 Agent 靠不靠谱,用的不是准确率,是它有没有在动。同一个工具跑 600 毫秒,你可以让用户盯着空白等 600 毫秒,也可以在第 100 毫秒就告诉他扫到第几片了。后者不会让任务更快,但会让它看起来更可信。

    工具固定 600 毫秒、分 6 段推进,对比“只在结束时发一次结果”与“每段用 onUpdate 推送”两种模式下的首次可见时间。

    事件流本身就是给界面的协议

    循环发出来的九种事件,本质上是一份界面渲染协议:agent_start 代表开始思考,turn_start 代表新的一轮,message_update 携带文字增量,tool_execution_start 代表开始调用工具,tool_execution_update 携带进度,tool_execution_end 代表工具完成。

    不需要额外设计一套状态管理,也不需要在后端和前端之间同步一个“当前状态”字段。把事件按顺序重放,界面自然就是对的。这是事件驱动的最大好处:不存在状态漂移,只有事件缺失。

    这也是一个排查思路。界面上显示和实际不符,一定是某类事件没被处理,而不是“状态不对”。事件是唯一真相源,状态只是它的投影。

    把进度推出去

    execute: async (id, params, signal, onUpdate) => {
      for (let i = 1; i <= 6; i++) {
        await scanShard(i, signal)
        onUpdate?.({
          content: [{ type: 'text', text: `已扫描 ${i}/6 个分片` }],
          details: { done: i, total: 6 },
        })
      }
      return { content: [{ type: 'text', text: '扫描完成,3 条异常' }], details: { done: 6, total: 6 } }
    }
    

    背景:onUpdate 的第三个参数是 AbortSignal。长任务里必须把它往下传——用户点了取消,扫描循环要真的停下,而不是等它自己跑完。这是中断语义能不能落地的地方,第 8 篇会展开。

    508 毫秒的差,感知上是两个产品

    我测了两种模式。不报进度时,第一次有东西可显示是在 611 毫秒——也就是工具结束那一刻。用 onUpdate 每 100 毫秒推一次,第一次可见是 103 毫秒。

    508 毫秒的差距在人类感知里是明显的:低于 200 毫秒感觉是“立刻”,超过 500 毫秒开始怀疑“是不是卡了”,超过 1 秒很多人会刷新页面。

    更重要的不是感知,是可中断性。界面上一片空白的时候,用户不知道该不该等;一旦看到“正在扫描 3/6”,他就能判断这次还要多久,要不要停。

    首次可见时间对比条形图:仅结束事件 611 毫秒,onUpdate 流式 103 毫秒

    首次可见时间:不报进度要等 611 毫秒,用 onUpdate 只要 103 毫秒。总耗时几乎一样,感知完全不同。

    两种模式的实测对照

    指标 仅结束事件 onUpdate 流式 差值
    首次可见(毫秒) 611 103 -508
    总耗时(毫秒) 612 610 -2
    中间事件数 0 6 +6
    用户可判断剩余时间 否 是 —

    别把进度做成噪音

    进度推送不是越多越好。每 10 毫秒推一次,界面会被高频更新拖垮,用户也读不出信息。合理的粒度是「每完成一个有意义的单位推一次」——一个分片、一批文件、一条记录。

    还有一点:进度内容要能回答「还要多久」。只推「正在处理」没有信息量,推「3/6,预计还要 300 毫秒」才有。所以 details 里带上 done 和 total,让界面自己算百分比。

    最后是节流。如果工具的天然粒度太细(比如逐行处理十万行日志),在工具内部累积到一定量再推,而不是每行推一次。

    放到服务端要考虑的三件事

    本地跑和放到服务端是两回事。第一件是传输:这些事件要转发给浏览器,SSE 是最省的方案,因为它是单向推送,不需要 WebSocket 的双向握手,也能穿过大多数代理。转发的时候只挑界面需要的字段,不要整个消息对象序列化——一条 tool result 里可能塞了几百 KB 的日志。

    第二件是顺序。事件的价值在于顺序,乱序重放会得到错误的界面。用一条自增序号或单调时间戳标注每个事件,前端检测到跳号就重新拉一次状态快照,而不是硬着头皮往下画。

    第三件是断线。长任务里网络中断几乎必然发生,用户刷新页面之后不该看到一个空界面。要把事件落盘(哪怕只是追加到一个本地 JSONL),重连时按 last-event-id 补发。这件事在脚本编排时代不需要考虑,因为脚本是同步阻塞的;一旦换成循环,任务可能跑十几分钟,补发就成了必需品。

    顺带一个反直觉的结论:事件持久化的成本比日志持久化低。因为事件是结构化的,落盘体积小;而日志是自由文本,事后要靠正则去猜当时发生了什么。把事件存好,等于把现场存好了。

    事件流打通之后,下一层问题就来了:上下文会越积越长。第 6 篇讲消息的两套模型,以及为什么压缩必须在 Agent 层做、不能写进提示词。

    引用与来源

    • pi-agent-core 事件类型表 — 九种事件与 tool_execution_update 语义
    • AgentToolUpdateCallback 类型定义 — 版本 1.4.0
    4小时前

最新文章

  • Pi Agent Core 实战(10/10):循环上线之后2026-11-01
  • Pi Agent Core 实战(9/10):把死调用交给模型2026-11-01
  • Pi Agent Core 实战(8/10):中断与排队2026-11-01
  • Pi Agent Core 实战(7/10):拦截钩子2026-11-01
  • Pi Agent Core 实战(6/10):消息有两套2026-11-01
  • Pi Agent Core 实战(5/10):把事件流当 UI2026-11-01

热门专题

  • AI 大模型
  • Claude 实战
  • 前沿观察
  • 安全攻防

热门标签

AI编程claude大模型AIAI Agent人工智能开源项目Gemini开发者工具开源Claude CodeGitHub开源工具AI工具开发工具谷歌openaideepseek提示词工程cursoranthropic网络安全AI应用ChatgptAI开发agentAI安全自动化智能体AI智能体

网站统计

  • 日志总数:29561
  • 评论总数:7
  • 标签总数:17945
  • 用户总数:3675
  • 最后更新:2026-11-01

© 2023-2026   IT资源栈   粤ICP备2021152721号-5