来学习下pi agent的原理

之前也偶尔用户pi agent这个工具,当然这个还是个很庞大的系统,我也只能一点点学
首先是工具系统,默认的核心工具主要是四个 read,write,edit,bash

read 主要是读取文件

输入结构式这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"path": "src/app.ts",
"offset": 1,
"limit": 200
}
- path:相对或绝对路径
- offset:从第几行开始,1 开始计数
- limit:最多读取多少行
内部的原理大致是
```text
解析路径
→ 检查文件是否可读
→ 判断是文本还是图片
→ 读取内容
→ 按 offset/limit 截取
→ 限制输出大小
→ 返回给模型

对于文本,Pi 将文件按照 UTF-8 解码并拆分为行。默认最多返回前 2000 行或 50KB,哪个先达到就按哪个截断,并告诉模型下一次应该使用哪个 offset 继续读取。

1
[Showing lines 1-2000 of 3540. Use offset=2001 to continue.]

write 创建文件或完整覆盖

输入

1
2
3
4
{
"path": "src/Hello.php",
"content": "<?php\n\necho \"hello\";\n"
}

主要原理

1
2
3
4
5
解析目标路径
→ 创建不存在的父目录
→ 进入该文件的 mutation queue
→ 完整写入 content
→ 返回写入结果

如果文件已经存在,则完整覆盖。它不是“追加”,也不是“局部修改”。
Pi 会针对同一个文件建立 mutation queue,防止多个并行工具同时修改同一个文件导致内容互相覆盖:

1
2
3
4
5
write A ──────┐
├→ 同一个文件顺序执行
edit A ──────┘

write B ─────────→ 不同文件可以独立处理

edit 精确替换已有代码

这是四个工具中设计最讲究的一个。
输入
当前 Pi 支持一次提交多个互不重叠的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"path": "src/UserService.php",
"edits": [
{
"oldText": "public function find($id)",
"newText": "public function find(int $id): ?User"
},
{
"oldText": "return $this->users[$id];",
"newText": "return $this->users[$id] ?? null;"
}
]
}

内部原理

1
2
3
4
5
6
7
8
9
读取原文件
→ 去掉 BOM
→ 将 CRLF/CR 统一成 LF
→ 在原始文件中寻找每个 oldText
→ 检查每段是否唯一、是否重叠
→ 执行全部替换
→ 恢复原来的换行符和 BOM
→ 写回文件
→ 生成 diff 和 unified patch

关键要求是:oldText 必须精确匹配,而且在原文件里唯一。
例如文件中有两个:

1
return null;

模型如果只提交:

1
2
3
4
{
"oldText": "return null;",
"newText": "throw new RuntimeException();"
}

工具会拒绝,因为不知道应该修改哪一个。
模型必须提供足够的上下文:

1
2
3
4
{
"oldText": "public function load(): ?User\n{\n return null;\n}",
"newText": "public function load(): ?User\n{\n return $this->repository->first();\n}"
}

为什么采用精确替换

  • 它相当于一种轻量级“乐观锁”:
  • 模型看到文件版本 A
  • 模型根据 A 生成 oldText
  • 如果调用工具前文件被改成版本 B
  • oldText 匹配失败
  • 工具拒绝覆盖,要求模型重新读取
    这比让模型直接按行号修改更安全,因为文件新增一行后,行号可能全部偏移。
    执行成功后,Pi 同时生成:
  • 面向终端显示的彩色 diff
  • 标准 unified patch
  • 第一处变化的行号

bash 给模型提供“手脚”

输入

1
2
3
4
{
"command": "php -l src/UserService.php && php tests/run.php",
"timeout": 30
}

原理:

1
2
3
4
5
6
7
找到当前平台的 Shell
→ 在工作目录启动子进程
→ 注入环境变量
→ 同时监听 stdout/stderr
→ 实时把输出推送到 TUI
→ 等待退出码
→ 将结果返回给模型

它可以完成:

  • rgfindls 搜索项目
  • 运行测试
  • 调用编译器
  • 执行 Git 命令
  • 安装依赖
  • 启动构建脚本
  • 执行任意系统程序
  • 输出处理
    read 保留“开头”不同,bash 更适合保留“结尾”。
    因为测试、编译和日志输出的错误通常在末尾。因此 Pi 保留最后 2000 行或最后 50KB。如果发生截断,完整输出会另外保存到临时文件。

四个工具如何协作

假设用户说:
给项目增加一个 /health 接口并运行测试。

模型可能按下面的流程工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. bash
rg "Route|router" src

2. read
读取找到的路由文件

3. edit
精确插入 /health 路由

4. write
创建新的 HealthController.php

5. bash
php -l ... && php tests/run.php

6. read
如果测试失败,读取相关源文件

7. edit
修复问题

8. bash
再次运行测试

这就是 Coding Agent 的核心闭环:

1
2
3
4
5
观察(read/bash)
→ 决策(LLM)
→ 修改(edit/write)
→ 验证(bash)
→ 再观察

大致学一下这个工具的工具逻辑