Site logo
Published on

Pi 源码解读(三):Tool Calling 如何真正读写文件?

Authors
  • avatar
    Name
    Stone
    Twitter
本篇目录

上一课讲了“模型 → 工具 → 结果 → 模型”的循环。这一课只放大中间一步:模型返回一个工具请求,为什么就能让程序读到文件?

NOTE

工具可以先理解成:一份给模型看的说明书,加上一个由程序执行的函数。

模型根据说明书提出请求,Pi 再调用对应函数。模型没有亲自运行那段函数。

本课对照 Pi v0.84.1。沿用修改 Vue 页面标题的例子,伪代码只表达主要思路,不是完整源码。

一、模型先要知道“有哪些工具可用”

假设你要求:

把 src/App.vue 的页面标题从 Hello 改成你好。

文件内容还没提供给模型,所以它可能先请求读取。但在这之前,Pi 得把当前可用工具的说明交给模型。以 read 为例,先认识四个字段:

字段大白话read 的例子
name工具叫什么?read
description工具能干什么?读取文件内容。
parameters调用时要填写什么?文件路径 path,也可以指定读取范围。
execute收到请求后,程序实际做什么?执行文件读取,返回内容。

前三项属于给模型的工具说明;最后一项留在运行程序这一侧。 Pi 的基础 Tool 类型定义名称、描述和参数;AgentTool 在其基础上增加执行方法。不是把执行函数发送给模型,让模型自己运行。12

这很像你阅读接口文档:你知道接口用途、需要哪些参数,但真正查询数据的代码仍然在服务端。

二、模型不是喊一句口号,而是提交一张“请求单”

模型看过用户需求和工具说明后,可能返回这样的意思:

调用编号:1;工具:read;参数:path = src/App.vue。

这里用中文展开,是为了方便理解。真实返回是结构化的 Tool Call,包含调用编号、工具名称和参数等信息;不是 Pi 从一段普通回答里猜测“它是不是想读文件”。1

因此,下面两件事不同:

“我准备读取文件。” 这可以只是一段展示给用户的文字。

一条 read 工具请求。 这才是运行时可以识别和处理的动作请求。

工具能力由开发者提供;这一轮选择哪个工具、填写什么参数,则可以由模型根据任务提出。 如果模型编造了一个没有注册的工具名,程序不会凭空长出那个能力。3

三、Pi 收到请求后,做什么?

先找工具,再检查参数,最后才执行。

例如请求里写的是 read,Pi 就在当前可用工具中找这个名字。找不到,返回工具错误;找到了,再检查有没有缺少必填参数、参数结构是否符合要求。这里的参数规则叫 Schema,你可以把它理解成表单的填写规则。3

把主流程缩成伪代码就是:

收到工具请求:
    根据名字找到工具
    找不到工具,或参数不合规 → 生成错误结果
    否则调用执行函数 → 得到结果,或捕获执行错误
    给结果带上对应的调用编号
    把结果加入对话记录
    供下一轮模型调用使用

这里省略了取消、扩展钩子等细节。你现在只需要看清:模型返回的是数据,Pi 根据数据调用已经存在的函数。

对于本例,真正读文件的部分在 read.ts。它的默认实现会检查文件是否可读,并通过文件系统接口读取内容;文件很大时,还会处理输出截断。你不需要先研究这些分支,先找到“执行函数最终读了文件”这一点。4

四、读到了,还得把结果“送回去”

假设工具读到页面里有 <h1>Hello</h1>。

工具执行完成只意味着运行程序拿到了这个内容。要让模型基于它提出修改,Pi 还要把内容包装成 Tool Result(工具结果),放进后续模型使用的记录里。5

你可以把它理解为给刚才那张请求单补上回执:

对应调用编号:1;结果:这是文件内容;是否出错:否。

源码中的 toolCallId 就是这种对应关系。假如模型请求读取两个文件,每个结果都需要明确对应哪次请求,不能只靠一句“读取成功”来猜。1

如果文件不存在呢? Pi 会把正常工具异常处理路径中的错误转成带错误标记的结果,交给模型。模型才有机会重新找路径或询问用户;不是工具失败后,运行时替它偷偷编造一个成功结果。5

所以,工具结果不能只是“给人看的日志”。它也是下一步决策需要的证据。

五、到了改文件这一步,不能只检查参数类型

模型读到文件后,可以再请求 edit。这个工具不仅需要文件路径,还需要一组“旧文字 → 新文字”的替换信息。本课版本的工具说明要求旧文字能在原文件中唯一定位。6

例如文件中出现了很多次 Hello,只说“替换 Hello”就容易表达不清。教学上更好的请求是指定标题那一段:把 <h1>Hello</h1> 换成 <h1>你好</h1>,并确认它足够明确。

这里可以分清三个问题:

参数是否合规? 例如有没有文件路径、替换信息的结构对不对。

操作是否适用? 例如文件是否存在,目标文字能不能被明确找到。

操作是否允许? 例如这个文件是不是本次任务允许修改的范围。

前两个问题处理好了,也不自动解决第三个。参数校验不是权限控制。 路径是一个合法字符串,不代表这个路径上的文件就应该允许访问;是否需要审批、限制路径或隔离运行,要由具体系统另外设计。

六、回到源码和面试,只抓住这条线

第一遍先看 read.ts 里的工具名称、说明、参数和执行函数,再到 agent-loop.ts 里找“根据名字选工具,调用 execute”的位置。

需要定位时,再展开这三个源码路标

prepareToolCall:找到工具,准备并验证参数。

executePreparedToolCall:调用执行函数,处理执行结果或异常。

createToolResultMessage:把处理后的结果包装成工具结果消息。

它们都在本课版本的 packages/agent/src/agent-loop.ts 中。不需要背函数名,能对应到流程即可。

面试时,在你确实对照源码看过之后,可以这样讲:

我主要跟了 Pi 的工具调用链路。工具一边提供名称、说明和参数规则给模型,另一边提供实际执行函数。模型返回工具名和参数后,运行时会查找工具、校验参数,再执行函数,并把带调用编号的结果放回上下文。这里我特别注意到,参数校验只能检查调用结构,不能代替权限控制。

一个小练习

read 已注册,模型填写的路径也是字符串,但文件不存在。这属于参数结构错误,还是执行错误?下一轮模型应该得到什么?

想过之后再看答案

在其他参数均合规的前提下,这是文件读取时的执行错误,不是“路径必须是字符串”这条规则出了问题。模型应该拿到与这次调用对应的错误结果,再决定如何继续。它不能因为自己发出了读取请求,就当成已经读到了文件。


这一课记住:工具说明让模型知道“能怎么用”,执行函数负责“真正去做”,工具结果告诉模型“到底发生了什么”。

参考源码

Footnotes

  1. Pi v0.84.1 · packages/ai/src/types.ts ↩ ↩2 ↩3

  2. Pi v0.84.1 · packages/agent/src/types.ts ↩

  3. Pi v0.84.1 · packages/agent/src/agent-loop.ts ↩ ↩2

  4. Pi v0.84.1 · packages/coding-agent/src/core/tools/read.ts ↩

  5. Pi v0.84.1 · packages/agent/README.md ↩ ↩2

  6. Pi v0.84.1 · packages/coding-agent/src/core/tools/edit.ts ↩