我复现了 Pi 的第一个 Agent 内核,才发现重点不是工具多不多

lxiol

原文链接:https://mp.weixin.qq.com/s/AlUrCBkRYCzU0bQkQanSaw

一次 root commit 复盘:从 a74c5da1 拆出最小 agent runtime,用 mock model、safe tools 和验证脚本,把看懂架构推进到真的跑起来。

一次 root commit 复盘:从 a74c5da1 里拆出最小 agent runtime,用 mock model、safe tools 和验证脚本,把“看懂架构”推进到“真的跑起来”。

我这两天盯着 Pi 的 root commit:a74c5da1

不是那种“扫一眼目录结构”的看法。说实话,目录结构最容易骗人。你看见 agenttoolstuisession,很容易觉得自己懂了;但一到手上复现,问题马上来了:最小的 agent 到底需要几块东西?

我最后做了一个很小的 TypeScript 复现:

1
reproductions/a74c5da1/minimal-agent-runtime/

它没有真实 OpenAI API,没有 API key,没有真实 bash。只有一条最小路径:

1
用户消息 -> 模型决定调用工具 -> agent 执行工具 -> tool result 写回历史 -> 模型继续回答

先别急着嫌它小。这个“小”,刚好是重点。

最小 agent runtime 闭环

最小 agent runtime 闭环

● ● ●

先把最小闭环抓住

我一开始以为 root commit 里最值得看的,是工具系统。

毕竟里面有 readlistbashglobrg 这些东西,看起来很 coding-agent。但复现到一半,我收回这个判断。工具当然重要,可它不是第一层问题。

第一层问题是:agent runtime 怎么让模型、工具、状态三者接上?

这次复现里,我把最小闭环压成 6 步:

  • 01
    用户输入一条消息。
  • 02
    agent 把用户消息写入 message history。
  • 03
    agent 调用模型函数,把历史消息和可用工具传进去。
  • 04
    模型返回 tool call。
  • 05
    agent 执行工具,并把 tool result 写回 history。
  • 06
    agent 再次调用模型,拿到最终回答。

这 6 步跑不通,工具再多也只是工具箱。它还不是 agent。

我给自己设了一个很硬的判断标准:必须证明 tool_result 发生在最终 assistant_message 之前,而且这个 result 真的进入了 agent history。否则就只是“看起来像”。

这个判断挺朴素,但很管用。

● ● ●

为什么不用真实模型?

这次我故意没接真实模型。

原因很简单:真实模型会带来 3 个干扰项。

  • 网络。
  • API key。
  • 模型输出不稳定。

这些都不是当前问题。当前问题是 runtime 的控制循环,而不是 provider SDK。

所以我写了一个确定性的 deterministicMockModel。它的行为非常死板:

  • 第一次调用,如果 history 里还没有 tool result,就要求调用 echo
  • 第二次调用,如果看到了 tool result,就给最终回答。

听起来很玩具,对吧?但它刚好模拟了 tool loop 最关键的分叉:

1
messages without tool result -> model asks for tool messages with tool result    -> model gives final answer

这事儿有个好处:模型不再是黑盒变量。事件顺序、历史写入、中断行为,全都能被脚本断言。

我现在越来越觉得,学习 agent 框架时,第一版不要急着接真实 LLM。先把 runtime 行为变成确定性的,反而更容易看清骨架。

● ● ●

Agent.ask() 才是这次复现的中心

复现项目里最重要的函数是 Agent.ask()

它不像一个普通请求函数。它更像一个小型调度器:

  • 接收用户输入。
  • 写入内存消息历史。
  • 发出 user_message 事件。
  • 创建 AbortController
  • 调用 modelFn
  • 遇到 tool call,就执行工具。
  • 把 tool result 写回 history。
  • 再调用一次模型。
  • 最终发出 assistant_message

这就是 agent runtime 的骨架。

Agent / Model / Tool / Event 的职责边界

Agent / Model / Tool / Event 的职责边界

这里最重要的边界是这几个:

模型 负责决定下一步是说话,还是调用工具。

agent 负责维护历史、执行工具、广播事件、控制循环。

工具 只负责接收参数并返回结构化结果。

receiver / renderer / session 只消费事件,不应该反过来侵入 agent loop。

这个边界一旦想清楚,Pi 后面的几层关系就顺了很多:agent 是运行内核,coding-agent 是更具体的工程能力组合,ai 是模型/provider 适配,tui 是展示层。它们不是一坨东西。

讲真,这个结论看起来平平无奇,但手写一遍之后,味道就不一样了。

● ● ●

工具系统:我刻意没复现 bash

root commit 里已经有 bash,但这次复现我没做。

不是因为做不了,而是因为当前阶段不该做。学习目录里直接放任意命令执行,收益不高,风险倒是不小。

所以这版只保留了安全子集:

  • echo
  • readText
  • listDir

echo 用来稳定验证 tool loop。readText 和 listDir 用来保留“工具能接触外部环境”的感觉。

真正要复现的是 executeTool() 这层协议:

  • 找不到工具,返回 error result。
  • 参数不合法,返回 error result。
  • 工具执行失败,返回 error result。
  • 中断发生,返回 interrupted/error 信息。

注意这里的关键词是“返回”。工具错误不能随便把整个进程炸掉。外部动作失败,本身也应该成为模型可见的上下文。

这对 coding agent 特别重要。因为 coding agent 做的事天然容易失败:文件不存在、命令失败、参数错、用户中断、权限不够。失败不是异常路径,失败就是日常路径。

扯远了,回到 runtime。

● ● ●

事件流其实是在给后续模块留接口

这次没有做 renderer,也没有做 JSONL session。

但我保留了 AgentEventReceiver

为什么?因为事件流是 runtime 对外的稳定合同。

事件流把 runtime 和外部系统拆开

事件流把 runtime 和外部系统拆开

验证脚本里收集到的事件顺序是:

1
user_message assistant_start tool_call tool_result assistant_start assistant_message

这串事件一旦稳定,后面就好办了:

  • console renderer 可以把它打印出来。
  • JSON renderer 可以把它转成机器可读输出。
  • TUI 可以把它做成交互界面。
  • session persistence 可以把它写成 JSONL。
  • test harness 可以拿它做断言。

所以,虽然当前复现没有实现 session,但它已经给 session 留好了入口。

我以前看这类代码时,容易把 event receiver 当成“为了 UI 写的回调”。现在看,它更像是 runtime 和外部世界之间的一条隔离线。没有这条线,agent loop 很快就会被日志、UI、落盘、测试搅在一起。

● ● ●

验证脚本决定了什么叫完成

这个 change 最有价值的地方,不是写了多少代码,而是把 implementation complete 这句话变硬了。

不是文件写完就完成。不是 OpenSpec 文档齐了就完成。必须能跑:

1
npm run verify

验证脚本覆盖了 3 件事:

  • 01
    tool loop 的事件顺序正确。
  • 02
    tool result 在最终 assistant message 之前出现,并且写入了 agent history。
  • 03
    unknown tool、invalid args、abort signal 都能被稳定处理。

复现完成度验证边界

复现完成度验证边界

我觉得这点对学习开源项目特别重要。

很多时候我们说“我理解了”,其实只是“我读过了”。这俩差很多。

一个很实用的标准是:能不能写出一个最小复现,并让验证脚本替你守住关键行为?

能,就算往前走了一步。

不能,那就说明还有地方是脑补的。

● ● ●

这次真正学到的 4 件事

第一,agent 的核心不是 prompt,也不是工具列表,而是控制循环。

没有这条路径:

1
user intent -> model decision -> tool execution -> state update -> model continuation

就只是一次聊天请求外加一个函数调用入口。还差点意思。

第二,message history 是 agent 的状态中心。

模型不会自动“记得”工具做了什么。它只能通过下一次请求里的消息历史看到 tool result。所以 agent 的职责之一,就是把外部世界发生过的动作转成模型能消费的消息。

第三,事件流是 runtime 对外的合同。

UI、日志、session、测试不应该各写各的入口。它们最好消费同一种事件。否则越往后越难拆。

第四,安全复现比完整复刻更适合学习。

不做真实 bash,没有削弱理解。反而让注意力集中在工具协议、错误回写和 continuation 上。等这个 loop 稳了,再加危险工具才更靠谱。

● ● ●

它还没做什么?

这版仍然很小。有很多东西刻意没做:

  • 没有真实 LLM provider。
  • 没有 streaming token。
  • 没有多轮 session 落盘。
  • 没有 JSONL replay。
  • 没有 console / JSON / TUI renderer。
  • 没有真实 shell、glob、rg。
  • 没有工具权限边界和命令审计。

这些不是遗漏,是阶段边界。

当前 change 只负责把最小 runtime 跑通。后面可以继续拆:

  • a74c5da1-reproduce-agent-session-persistence
  • a74c5da1-reproduce-agent-rendering-modes
  • a74c5da1-reproduce-agent-tool-system
  • a74c5da1-reproduce-pod-agent-integration

每一层都可以接到这个小内核上。

● ● ●

写在最后

a74c5da1-reproduce-minimal-agent-runtime 的价值,不在代码量。

它的价值是把 Pi 初版 agent 的最小运行形态,从“我看过这些文件”变成了“我能跑出这条路径,并且能验证它”。

最后我给这次复现留了一个五行总结:

1
Agent 维护状态 Model 决定动作 Tool 连接外部世界 Event 暴露运行过程 Verification 固化学习结果

这就是我继续拆 Pi 的起点。

本文转载自微信公众号,如有侵权请联系删除。

  • 标题: 我复现了 Pi 的第一个 Agent 内核,才发现重点不是工具多不多
  • 作者: lxiol
  • 创建于 : 2026-06-20 01:24:43
  • 更新于 : 2026-06-20 01:24:43
  • 链接: https://blog.lxiol.cn/2026/06/20/我复现了-Pi-的第一个-Agent-内核才发现重点不是工具多不多/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。