Pi Agent Core 实战(1/10):400 行循环替掉 LangGraph

用 LangGraph 和 LangChain 搭 Agent,最容易失控的地方不是图写得多复杂,而是每一步该做什么,被编排代码提前决定了。模型拿到的是一个已经切好的流程,它只能在节点内部说话,不能决定下一个节点是谁。我把同一批 30 个故障任务分别交给固定脚本编排和 pi-agent-core 循环跑了一遍,结果差得有点难看。

实验在本地可复现:pi-agent-core 1.4.0,注入确定性 streamFn 取代真实模型,避免把结论建立在某次模型抽样的运气上。两套编排器看到的信息完全一致,差别只在谁决定下一步。

先把问题说清楚:不是图难写,是决定权放错了地方

LangGraph 的表达方式是状态机:你定义节点、定义边、定义在什么条件下走到哪个节点。这套东西在流程稳定的场景里很好用——审批流、ETL、固定的多步检索,路径是已知的,图的形状就是业务的形状。

问题出在流程不稳定的场景。真实排障任务的路径不是提前知道的:日志目录可能不存在,分析脚本可能在子目录,脚本可能跑完把判断交还给你,也可能直接算术报错。这些分支在写图的时候想不到,想到了也不想为每一种写一条边。于是最常见的做法是:把所有不确定都收敛成一个节点,让它固定调用一个脚本。

这一步做完,Agent 就退化成了 cron。它还在跑,还在调模型,但模型能做的只剩「把脚本输出复述一遍」。决定权已经被搬到编排代码里了。

四个症状,一个病根

这类系统往往同时出现几个看起来无关的症状:脚本返回“需要模型判断”时没人接;提示词里反复叮嘱不要做某事,但工具里根本没有替代手段;脚本调用参数写死,多一步就得多加一个分支;子进程不传工作目录,脚本里的相对路径静默失败。

四个症状对应四种缺失:缺少接管分支、缺少能力、缺少决策空间、缺少运行环境契约。它们的共同前提是同一件事——宿主替模型做了决定,却没给模型做决定所需的工具和反馈。

所以换框架不是目的。真正要改的是把决定权还回去:给模型探索的工具,给模型可读的失败信号,然后接受它会走弯路。pi-agent-core 值得看,是因为它把这套东西做到了极端——循环小到能一次读完,工具只有 Read/Write/Edit/Bash,其余全部交给模型。

实验设计:30 个任务,两种编排器

为了让比较有意义,我没有拿“我的系统”和“别人的系统”对比,而是造了一批任务,故障是真实注入的。30 个任务分六类,每类的失败方式都不一样:

最理想的一类有分析脚本、脚本正常返回;第二类只有日志没有脚本;第三类目录压根不存在;第四类脚本因为除零返回退出码 4;第五类脚本主动返回 2 并把判断交还给模型;第六类脚本藏在 scripts/ 子目录里,根目录找不到。

编排器 A 是脚本式:调用平台自带的固定入口,参数固定为 –log-dir 与 –output-json,退出码只认 0 和 2,其余一律算失败。编排器 B 是 pi-agent-core 循环:给模型 ls、read、bash 三个工具,让它自己看目录、自己找脚本、自己决定读日志还是跑脚本。

编排器 A 的核心:一个退出码白名单

r = subprocess.run([sys.executable, 'analyze.py', '--log-dir', a.log_dir], cwd=a.log_dir)
if r.returncode == 0:
    return parse(r.stdout)
if r.returncode == 2:
    return {'verdict': 'need_llm'}   # 交还给模型,但宿主没有接的逻辑
raise RuntimeError(f'exit {r.returncode}')  # 其余全部算失败

注意:这段代码里最贵的一行是 raise。它把“脚本说不清楚”统一压成“失败”,模型拿到的是一个没有信息量的异常,而不是“我需要你看一下日志”。所有下游的重试、降级、人工介入,都建立在这个被压扁的信号上。

结果:6 比 25,差距全在意外情况上

30 个任务,脚本式通过 6 个,通过率 20%。这 6 个正好是最理想的那一类——有脚本、脚本正常、参数对得上、目录存在。换句话说,它只在“什么都不出错”的时候工作。

循环式通过 25 个,另外 5 个是目录不存在的那一批:模型打开目录、拿到 ENOENT、停下来报告“目录不存在,需要人工确认路径”。这 5 个不算完成,但也不算失败——它给出了可行动的原因,人接手只需要一分钟。按“有结论”算,是 100%。

24 个失败的构成值得单独看一眼,因为它揭示了脚本编排真正的成本结构。

脚本编排 24 个失败的构成横向条形图:无分析脚本 10 个、路径不存在 5 个、退出码不在白名单 5 个、交还模型未处理 4 个

脚本编排 24 个失败的构成:真正“跑错”的只有 5 个退出码问题,其余 19 个是能力缺失——找不到脚本、路径不对、把判断交出去没人接。

19 个失败根本不是“脚本算错了”,而是“系统不知道该干什么”。找不到脚本、目录不对、判断交出去没人接——这三类在脚本编排里是死路,在循环里只是下一次工具调用。

这也是为什么加更多分支解决不了问题:每加一个分支,你是在为一种已知的意外写代码,而意外的种类是开放的。

六类任务,两种编排器的通过数

任务类型 数量 脚本式通过 循环式通过 循环式带原因上报
有脚本·正常 6 6 6 0
只有日志·无脚本 6 0 6 0
目录不存在 5 0 0 5
脚本返回 4 5 0 5 0
脚本交还模型 4 0 4 0
脚本在子目录 4 0 4 0

同一个任务集,同样的信息,唯一变量是“谁决定下一步”。

第三列和第四列的对比很直接:脚本式只在第一行有数。但真正说明问题的是第五列——循环式在目录不存在时没有硬闯,也没有编一个结论,它停下来报告了原因。

这一点比通过率更重要。能准确说出“我做不了,因为 X”,比“勉强做完但不知道对不对”值钱得多。前者可以自动化接管,后者只能靠人重新检查一遍。

为什么 400 行就够:把复杂度从框架搬到观测层

pi-agent-core 的循环层只有 1445 行 JavaScript,其中三分之一是类型声明和事件定义,真正的调度逻辑更少。它做的事情可以一句话说完:把上下文发给模型,模型要调工具就执行,把结果塞回去,直到模型不再要工具。

少的东西是明确的:没有内置 MCP,没有子 Agent,没有 plan 模式,没有沙箱,没有待办清单。官方态度是“不需要的就不做”,这些能力要么由上层产品补,要么由模型自己用 shell 和文件拼出来。

这不是省事,是转移。原来框架替你处理的东西——工具权限、超时、预算、失败归因、可观测性——现在全部落在你身上。所以第 10 篇会专门讲这块:极简框架的账不是不付,是换了个时间付。

背景:一个判断标准:如果你的流程有 20 个以上的确定节点、分支条件可以穷举、每一步的失败方式都知道,那状态机更好,别换。循环适合的是“下一步取决于这一步看到了什么”的场景——排障、调研、修复、探索式数据处理。

这个系列接下来做什么

后面九篇按一条线走:先把三层拆开看清楚(02),跑通最小 Agent(03),把工具设计对(04);然后进中级——事件流当 UI(05)、消息模型与上下文压缩(06)、钩子做权限与预算闸门(07)、中断与排队(08);最后回到工程现场,把脚本编排的死调用改成模型自主决策(09),再谈上线之后的可观测与回归(10)。

所有实验代码都在同一个仓库里,每个结论都能自己跑一遍。这是这个系列唯一想坚持的事:不写“据说如此”,只写“我跑出来是这样”。

引用与来源

C code80.ai · AI 编码 API 聚合 Claude / GPT 多模型统一接入,稳定不限速,按量计费,几行配置接入 Claude Code。 了解一下 ›

抢沙发

评论前必须登录!

立即登录   注册