Agentpath
动手31 / 31 节 · 预计 60 分钟

造你自己的 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,而在于它把这些东西摊开给你看了。 接下来该做的很简单:挑你最常干的那件事,用这一套把它做扎实, 然后按上面那个节奏迭代下去。

祝你造的东西好用。

延伸资料

这是最后一节 —— 该去造自己的了 🎉