1. 从一次“工具调用卡住”说起:Pi Agent 架构到底解决什么问题
如果你最近在折腾终端里的编码 Agent,大概率遇到过这种场景:让它读一个文件、改一处配置、再跑一条命令验证,结果它要么在工具调用之间“断片”,要么进程一重启上下文全丢,要么换个模型就报一堆字段不兼容。Pi Agent 就是冲着这些工程痛点来的——它是一个极简的终端编码 Agent 框架,核心定位是 Coding Agent Harness,设计哲学一句话概括:内核尽量小,扩展尽量强。
它把能力拆成三个核心包:@earendil-works/pi-ai负责统一 30 多家 LLM Provider 的调用;@earendil-works/pi-agent-core是 Agent 运行时,管工具调用循环和状态;@earendil-works/pi-coding-agent是面向编码场景的交互式 CLI,把工具、扩展、会话、UI 落地。适合谁?适合想把 Agent 循环嵌进自己工具链的工程师,也适合想搞懂“Agent Loop 和 Agent Harness 到底差在哪”的开发者。
这篇不空谈概念,我会沿着 Agent Loop(对话+工具的事件循环)和 Agent Harness(可持久化、可恢复的执行状态机)两条主线,把运行机制拆开,再给你能直接复制的配置片段和验证步骤。中间会对照 TaoToken 的统一 Key/API 通道做接入验证,这样你不用同时维护一堆 Provider 的密钥,就能把循环跑起来观察工具调用链路。
先说清楚一个心智模型:Agent Loop 是“一次对话里怎么转起来”,Agent Harness 是“转起来之后怎么不丢、怎么恢复、怎么并发隔离”。前者是引擎,后者是变速箱加行车记录仪。很多人只搭了 Loop 就上生产,结果一崩就全没了,问题就出在缺 Harness 这层。
2. 前置准备:用 TaoToken 统一 Key 打通多模型通道
在复现 Agent 循环之前,得先解决“模型从哪来”的问题。Pi 的pi-ai层支持 30 多家 Provider,但如果你每个都单独配 Key、单独处理 OAuth 刷新,光是鉴权就能耗掉半天。我的做法是用 TaoToken 作为统一入口,一个 Key 走多家模型,省掉多套凭证管理。
TaoToken 在这里扮演的是统一 API 通道:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。它的价值在于把 OpenAI 兼容协议、Anthropic 协议等收敛到一套 Base URL + Key 上,Pi 的 Provider 配置里只要指向这个端点,就能在 Claude、GPT、Gemini 之间切换而不用改代码。
你需要准备的东西不多:一个 TaoToken 的 API Key(在控制台创建,地址 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ),Node.js 18+ 环境,以及一个干净的测试目录。Key 的创建入口在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,建议单独建一个用于本地调试的 Key,方便随时吊销。
这里有个容易踩的坑:很多人把 Key 直接写进代码或提交到 git。正确做法是走环境变量,Pi 的models.getAuth()解析顺序是“显式传入 → Provider 已存储凭证 → 环境变量/OAuth”,所以环境变量是最省事又安全的一层。下面这段就是我会用的方式:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"配好之后先别急着跑 Agent,先用一条 curl 验证通道是通的,避免后面把网络问题误判成 Agent 逻辑问题:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "reply with ok"}], "max_tokens": 16 }'返回里能看到choices[0].message.content就说明通道没问题。这一步很关键,因为后面 Agent Loop 报错时,你要能快速区分是“模型通道挂了”还是“循环逻辑写错了”。如果你更想先在网页里确认模型可用性,也可以直接去模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 手动发一条消息试试。
3. 可复制配置:把 Agent Loop 和 Harness 接起来
这一节给你能直接落地的配置。Pi 的配置分两层:一层是模型 Provider 配置,一层是 Agent 运行配置。先看模型层,Pi 的pi-ai把 Provider 和 API 实现分离,多个 Provider 可以共享同一套 API 实现。用 TaoToken 的话,本质是走 OpenAI 兼容端点,所以配置长这样:
{ "providers": { "taotoken": { "api": "openai-completions", "baseURL": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "models": [ { "id": "claude-sonnet-4-20250514", "contextWindow": 200000, "maxOutputTokens": 8192 }, { "id": "gpt-4o", "contextWindow": 128000, "maxOutputTokens": 4096 } ] } }, "defaultProvider": "taotoken", "defaultModel": "claude-sonnet-4-20250514" }如果你用的是 Claude Code 那套生态,配置习惯会不太一样,通常放在~/.claude/settings.json或项目级.claude/settings.json里,走的是 Anthropic 协议。用 TaoToken 接入时,三件套要写全:Base URL、Key、Model ID。片段如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }注意这里 Base URL 和 Key 必须成对出现,只改一个会直接 401。Model ID 也要和 TaoToken 侧支持的模型名对齐,写错了会报 model not found 而不是鉴权错误,排查时别搞混。
再看 Agent 运行层。Pi 的 Agent Loop 是事件驱动的,工具执行默认走 parallel 模式:所有工具调用的前置校验串行进行,随后允许并发执行,但持久化的 toolResult 消息仍按 assistant 消息里声明的原始顺序落盘。这个细节很重要,它保证了并发执行不会打乱结果顺序。配置片段:
{ "agent": { "executionMode": "parallel", "maxTurns": 20, "tools": { "read": { "enabled": true }, "write": { "enabled": true }, "edit": { "enabled": true }, "bash": { "enabled": true, "executionMode": "sequential" } } }, "harness": { "sessionBackend": "sqlite", "sessionPath": "./.pi/sessions.db", "lanes": 2, "resumeOnStart": true } }这里有个关键点:只要一批工具调用里有任意一个被标记为sequential,整批都会退化为顺序执行。上面我把bash设成 sequential,是因为 shell 命令之间常有依赖,并发跑容易出竞态。而read、grep这类只读工具并发是安全的。
Harness 层的lanes是执行道数量,每条 Lane 维护自己的会话叶子节点、操作状态和消息队列。resumeOnStart打开后,进程重启会从 SessionTree 里恢复。这就是 Loop 和 Harness 的分工:Loop 负责“这一轮怎么转”,Harness 负责“转完的状态存哪、崩了怎么接着转”。
如果你要长期跑编码任务或 Agent 自动化,建议直接上 Coding Plan,省得自己维护会话后端和并发隔离,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
4. 验证请求:观察一次完整的工具调用链路
配置好了,现在跑一次真实请求,把事件流打出来看。Pi 的 Agent Loop 会产出一串结构化事件,理解这些事件是排查问题的关键。先写一个最小验证脚本:
import { createAgentSession } from "@earendil-works/pi-coding-agent"; const session = await createAgentSession({ provider: "taotoken", model: "claude-sonnet-4-20250514", cwd: process.cwd(), }); session.on("agent_start", () => console.log("[agent_start]")); session.on("turn_start", () => console.log("[turn_start]")); session.on("message_start", (m) => console.log("[message_start]", m.role)); session.on("tool_execution_start", (t) => console.log("[tool_start]", t.toolName, t.toolCallId) ); session.on("tool_execution_end", (t) => console.log("[tool_end]", t.toolName, t.result?.isError ? "ERROR" : "OK") ); session.on("turn_end", (t) => console.log("[turn_end] toolResults:", t.toolResults.length) ); session.on("agent_end", (a) => console.log("[agent_end] total messages:", a.messages.length) ); await session.prompt("读取 package.json 并告诉我 name 字段的值");跑起来后,你会看到类似这样的事件序列:agent_start→turn_start→ user 消息的message_start/end→ assistant 消息携带 toolCall →tool_execution_start/update/end→ toolResult 消息 →turn_end→ 新一轮turn_start→ assistant 基于工具结果响应 →turn_end→agent_end。
这里要重点观察三件事。第一,tool_execution_end的触发顺序可能和声明顺序不同(parallel 模式下按完成顺序触发),但落盘的 toolResult 消息顺序一定和 assistant 声明顺序一致。第二,message_update只在 assistant 消息上触发,携带流式增量,user 和 toolResult 消息没有这个事件。第三,如果工具抛异常,Pi 的约定是“成功返回内容,失败抛异常”,抛出的错误会被 Agent 捕获并转成isError: true的工具结果反馈给 LLM,而不是让整个循环崩掉。
验证成功的结果长这样:终端里能看到完整的工具调用链路,最后 assistant 给出package.json的 name 值。如果中途卡住,先看tool_execution_start有没有对应的tool_execution_end,没有就是工具执行挂起;如果有tool_execution_end但isError: true,去看工具返回的错误内容。
想更直观地对比不同模型在同一条链路下的表现,可以开两个终端,一个用 Claude,一个用 GPT,都指向 TaoToken 的同一个 Base URL,观察工具调用次数和 token 消耗的差异。模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 也能手动发同样的 prompt 做对照。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来对。我把踩过的坑列成对照表,你遇到时直接查。
401 Unauthorized:最常见。原因通常是 Base URL 和 Key 不匹配,或者 Key 没通过环境变量正确注入。检查TAOTOKEN_API_KEY是否 export 成功(echo $TAOTOKEN_API_KEY看有没有值),以及配置里的apiKeyEnv名字是否和实际环境变量名一致。如果用的是 Claude Code 那套ANTHROPIC_BASE_URL+ANTHROPIC_API_KEY,两个必须同时改,只改一个必 401。
local proxy failed / connection refused:这类报错说明请求根本没到 TaoToken 端点,多半是本地网络层或 Base URL 写错。先确认https://taotoken.net/api能通(用第 2 节的 curl),再检查配置里有没有多余的路径后缀,比如误写成/api/v1/v1。Pi 的 Provider 配置里 baseURL 只写到/api,具体路径由 API 实现层拼接。
reading 'choices' of undefined:这个报错说明响应体结构和你预期的不一致,通常是模型名写错导致返回了错误对象,或者 Provider 的 api 类型配错了。比如把 Anthropic 协议的模型配到了openai-completions实现上。解决方法是核对 Model ID 是否在 TaoToken 支持列表里,以及api字段和模型协议是否匹配。用curl单独打一次这个模型,看返回体里有没有choices字段。
OAuth token expired / refresh failed:Pi 的models.getAuth()支持 OAuth 自动刷新,且加锁防止并发重复刷新。但如果你混用了 OAuth 凭证和环境变量 Key,可能出现刷新逻辑走了 OAuth 分支而实际没有有效 refresh token。最省事的做法是本地调试统一用 API Key,别混 OAuth。如果确实要用 OAuth,确认凭证存储路径可写,刷新失败时清掉旧凭证重新授权。
工具执行卡住不返回:不是报错但更烦人。检查是不是某个工具被设成了sequential且内部有阻塞操作。parallel 模式下只要有一个工具标记 sequential,整批退化顺序执行,一个慢工具会拖住整批。排查时先把所有工具临时设成 parallel,看是否恢复。
Harness resume 后上下文错乱:多半是 SessionTree 的 leaf 指针没对上。Harness 的会话是一棵树,entry 通过 id/parentId 关联,当前工作位置是活跃叶子节点。如果手动改过 session 文件,leaf 可能指向了不存在的 entry。用lane.navigateTree()重新定位,或者干脆开新 Lane 重跑。
排查时记住一个原则:先分层定位。401 和 proxy failed 是通道层,reading choices 是协议层,OAuth 是鉴权层,工具卡住是执行层,resume 错乱是持久化层。分层之后,问题范围立刻缩小。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各协议的端点说明,对照着看能省不少时间。
6. 把循环跑稳之后:从 Loop 到 Harness 的工程收尾
跑通一次 Agent Loop 只是起点。真正让 Pi Agent 在生产里站住脚的,是 Harness 那层把“对话循环”升级成了“可审计、可恢复、可并发隔离的状态机”。我自己的经验是,本地调试阶段可以只用 Loop,但一旦涉及长时间运行、多会话、或者需要崩溃恢复,就必须把 Harness 的 Lane 和 SessionTree 用起来。
几个实用技巧。第一,压缩阈值别设太激进。Pi 的自动压缩在上下文超过contextWindow - reserveTokens时触发,保留最近消息的 token 数由keepRecentTokens控制。设太小会频繁压缩丢细节,设太大又容易撞上下文上限。我一般留 15% 到 20% 的 reserve。第二,扩展点session_before_compact可以接管摘要生成,如果你有更便宜的模型,用它做摘要能显著降本。第三,遥测 Span 里的pi.ai.request记录了 usage、cost、首字节延迟,跑对比评估时这些数据直接可用,别自己再埋一遍。
如果你要把这套东西嵌进 IDE 或自己的应用,走 RPC 模式(--mode rpc)比直接调 SDK 更稳,JSONL over stdin/stdout 的进程内协议对宿主环境侵入小。远程多用户场景则上pi-protocol+pi-server+pi-client三件套,会话租约分独占和共享两种模式,按需选。
最后回到接入这件事:不管你用哪种运行模式,模型通道都可以收敛到 TaoToken 这一层。一个 Key 管多家模型,切换模型不用改代码,评估对比时也能保证 baseline 和 candidate 走的是同一条通道,排除网络变量。需要长期跑编码 Agent 的话,Coding Plan 那套已经把会话后端和并发隔离打包好了,省得自己维护 SQLite 和 Lane 调度。把 Loop 跑通、把 Harness 配好、把通道统一,剩下的就是让 Agent 自己转起来了。