造你自己的 harness:从 Pi 毕业
把前面所有零件按自己的工作流重新拼一遍 —— 这才是这门课的目的。
学完这节你能做到
- 定义自己 harness 的边界:内核放什么、扩展放什么
- 拼出一个能日常用的 agent,并说明每个取舍的理由
- 给它写下第一版评测与预算上限
最后一节。把前面三十节的零件按你自己的工作流拼一遍 —— 这才是这门课的目的。不是学会用 Pi,是学会造一个只属于你的 harness。
先写清楚你的工作流
别急着写代码。先用大白话回答五个问题,写在纸上:
1. 我最常让 agent 做的三件事是什么?
2. 这三件事分别需要碰到什么(文件?集群?内部系统?外部 API?)
3. 每件事的「做完了」怎么判定?(这一条答不上来就没法评测,也没法闭环)
4. 哪些动作我绝对不接受它自作主张?
5. 我是在旁边看着,还是挂着让它自己跑?
第 3 和第 4 决定了工程量的八成。第 5 决定了防线要做到多硬。
内核最小化:什么必须硬编码
Pi 给的示范是:核心极小,其余全是扩展。你自己的 harness 照这个原则划线 —— 判断标准是「改这个东西需不需要重新想一遍整体」:
| 必须在内核 | 该做成扩展 |
|---|---|
| loop 与退出条件 | 具体的工具 |
| 上下文组装顺序(缓存前缀) | 提示词内容 |
| 事件流定义 | 界面与展示 |
| 会话存储格式 | 权限策略细节 |
| 硬上限(轮次/预算/超时) | 压缩策略 |
| 「拿不到确认时拒绝」这条默认值 | 记忆与检索 |
最后一行是特意放的:安全默认值属于内核。 它不能是某个扩展的可选行为 —— 那个扩展没装上的那天就是事故日。
组装:一份检查清单
按 L0→L4 的顺序过一遍,每一条都能指回具体某节:
循环与工具
- 三个退出条件都有(模型收尾 / 轮次上限 / 预算上限)+ 墙钟超时
- 工具错误原样回灌,写成「现象 + 原因 + 可行动作」
- 工具输出有上限、有分页、截断时说清楚
- 只读工具可并行,写入工具串行且幂等
-
content给模型、details给界面
上下文
- 前缀稳定:系统提示 → 项目指令 → 工具声明 → 历史
- 动态内容放最后一条消息,不进系统提示
- 有压缩策略,且知道它会让那一轮全价
- 长任务进展外置到文件
边界
- 动作分级落到代码,未知按高危
- 无 UI 时默认拒绝
- 真正的权限由 RBAC / IAM / 只读挂载提供
- 无人值守场景在容器或 VM 里跑,凭据最小且短期
可观测与评测
- 事件流能落成 trace(长度 + 错误标记 + 用量,不存全文)
- 三种 token 分开记,能算出缓存命中率
- 有「无进展」检测
- 有 10 个以上可自动判定的评测用例,改提示词/工具后会跑
一份最小实现
用 SDK 把这些拼起来,其实不长:
import { createAgentSession, ModelRuntime, SessionManager, DefaultResourceLoader,
defineTool } from "@earendil-works/pi-coding-agent"
const LIMITS = { turns: 60, usd: 5, wallMs: 30 * 60_000 }
export async function runTask(task: string, opts: { interactive: boolean; cwd: string }) {
const loader = new DefaultResourceLoader({
systemPromptOverride: () => MY_PROMPT, // 你的人格
agentsFilesOverride: [], // 不吃本机配置
extensionFactories: [gate(opts.interactive), telemetry()], // 边界 + 埋点
})
await loader.reload()
const { session } = await createAgentSession({
cwd: opts.cwd,
sessionManager: SessionManager.create(opts.cwd),
modelRuntime: await ModelRuntime.create(),
tools: opts.interactive
? ["read", "grep", "find", "ls", "edit", "write", "bash"]
: ["read", "grep", "find", "ls"], // 无人值守:只读
customTools: [myBusinessTool],
resourceLoader: loader,
})
const stop = watchLimits(session, LIMITS) // 超限就 abort 并说明原因
try {
await session.prompt(task)
} finally {
stop()
session.dispose()
}
}
注意 tools 那一行:交互与无人值守用的是两套工具集。
这一个 if 比一百行权限规则都有效。
什么时候该离开 Pi
诚实地说,多数人不需要离开。Pi 的扩展点足够深, 「造自己的 harness」在实践中往往就是「一个 SDK 封装 + 几个扩展 + 一份提示词」。
真正需要从零写的信号只有几个:
- 宿主环境根本跑不了 Node(嵌入式、特定运行时)。
- 循环本身要改:比如你要的不是 while 循环而是状态机 / 树搜索 / 多候选择优。
- 有非常特殊的合规要求:每一次模型调用都要过审计网关、要落到特定存储。
前两条之外的需求,扩展基本都能覆盖 —— 而自己写内核意味着上下文管理、 重试、压缩、事件流、会话树这些全部要重来一遍。你现在知道这些有多少细节了。
不是「我不用 Pi 了」,而是「我能说清 Pi 每个设计的理由, 并且知道我的场景里哪几处该改」。这门课如果只留下一样东西,应该是这个判断力。
往后怎么迭代
给自己定一个节奏,别做一次性工程:
| 频率 | 做什么 |
|---|---|
| 每次任务后 | 有没有哪一轮是白跑的?为什么 |
| 每周 | 看成本 p95 和失败分类,挑最贵的一类改 |
| 每次改提示词/工具 | 跑评测,看失败名单变了没 |
| 每月 | 复查一次防线:上限、拒绝默认值、凭据范围 |
| 出事之后 | 把这次事故变成一条评测用例 —— 这是唯一有复利的动作 |
最后一条最重要。事故 → 用例 → 回归,这个循环建立起来之后, 你的 agent 才会真正随时间变好,而不是每次改动都在赌。
你要把一个「在旁边看着用」的 agent 改造成「挂着自己跑」。下面哪些是必须重新设计的?
这门课的结论
回到第一节那句话:agent 就是一个 while 循环。三十节课下来, 你应该能同意后半句了 —— 难的从来不是循环,是循环外面那一圈。
那一圈现在你都造过一遍了:
工具设计 · 上下文预算 · 压缩 · 会话树 · 事件流
扩展与 skill · 权限门 · 沙箱 · 子 agent
SDK 与 RPC · 评测 · 可观测 · 成本
Pi 的价值不在于它是最好的 harness,而在于它把这些东西摊开给你看了。 接下来该做的很简单:挑你最常干的那件事,用这一套把它做扎实, 然后按上面那个节奏迭代下去。
祝你造的东西好用。