hello 我是逆境不可逃
模型一次可能提出多个工具请求。例如分析项目时,同时读取日志和配置;修改代码时,先编辑文件,再运行检查。执行这些请求,需要考虑它们之间有没有依赖,以及失败后还剩下什么状态。
Pi 的工具调度支持串行和并行。这篇从两个文件读取请求出发,梳理调度、结果排序和部分失败的处理思路。
哪些操作适合同时执行
假设读取日志需要 3 秒,读取配置需要 1 秒,两者都只读、互不依赖。这些时间只是用于说明流程。
text
串行:读取日志 3 秒 → 读取配置 1 秒 → 约 4 秒完成
并行:两个读取同时进行 → 约 3 秒完成
并行减少了等待,但需要先确认操作可以独立进行。
如果任务变成"修改配置,再读取修改后的内容",读取就依赖修改结果。同时启动时,读取可能先完成,拿到旧配置。两个工具同时改同一份文件,也可能相互覆盖。
Pi 当前根据执行配置和工具声明选择模式:整体配置要求串行,或者同一批调用中有任意工具声明必须串行,整批就按顺序执行;否则走并行路径。
这段调度逻辑不会自动分析两个文件路径是否相同,也不会推断业务依赖。它依赖工具声明和任务安排表达约束。串行执行能保证先后顺序,但如果后一步的参数必须根据前一步结果才能确定,通常还需要把前一步结果交回模型,再生成下一次请求。
并行之前,还有依次准备
Pi 的并行路径并不是一收到调用列表,就立即启动所有工具。它会先按顺序为每次调用做准备,包括查找工具、准备并校验参数,以及执行前置钩子。前置钩子是允许其他逻辑检查或阻止调用的位置。
普通情况下,流程可以写成下面的简化伪代码:
text
依次检查每个工具请求:
如果检查失败或调用被阻断:
保存错误结果
否则:
保存待执行操作
并行启动通过检查的操作
等待各操作结束
按原请求顺序整理结果
因此,如果 A 的执行前检查耗时 5 秒,即使 B 的检查很快,B 也要等准备阶段结束后才开始实际执行。这是当前流程的安排,并行带来的时间收益主要发生在后面的执行阶段。
依次准备让前置检查按固定顺序进行;代价是慢检查会延迟后续工具启动。不过,检查通过也不等于操作之间自动没有冲突,工具设计仍然需要考虑共享文件或外部状态。
先完成的工具,结果不一定先进入历史
继续看两个读取请求:
text
模型请求顺序:A 读取日志 → B 读取配置
实际完成顺序:B → A
结果消息顺序:A → B
Pi 把执行结束通知与正式结果消息分开处理。工具完成后,可以发出结束通知;等这一批操作全部结束,再按原始请求顺序生成工具结果消息。
这样,界面订阅者可以先知道 B 已完成,下一轮模型则在整批结果收齐之后继续。结果排列不会随着某次网络或磁盘操作快慢而改变。
调用编号仍然不可缺少。顺序让记录稳定,编号明确每条结果属于哪次请求,尤其是多个请求使用同一个工具名称时。
整批等待也有成本。如果 A 要 10 秒,B 只要 1 秒,当前路径不会在第 1 秒就拿 B 的结果启动下一轮模型请求。A 会拖住这一批。要实现部分结果提前驱动下一轮,就需要另外设计未完成调用的管理和后续消息合并。
一个调用被阻断,其他调用怎么办
假设 A 在前置检查时被阻断,B 检查通过,并且没有取消整个任务。
Pi 会为 A 生成错误结果,让 B 继续执行。结果整理后,模型能看到两件事:A 没有实际执行,B 得到了什么结果。
text
A:操作被阻断,错误结果
B:操作执行成功,成功结果
如果只返回 B,模型就缺少 A 的实际状态。明确反馈"执行前被阻断",也比笼统写成"失败"更有用,因为它与"执行了一部分后失败"有不同的恢复方式。
这里还要区分阻断与终止:阻断控制某次调用能否执行,终止控制是否继续模型循环。Pi 的工具批次只有在非空且所有结果都要求终止时,才通过这个条件结束后续循环;单个结果要求终止,不会自动取消同批其他调用。用户取消等其他停止条件则另行处理。
部分失败后,不能直接整批重跑
工具批次通常没有数据库事务那样的整体回滚能力。Pi 的这段调度不会因为 B 失败,就自动撤销 A 已经产生的修改。
例如:
text
A:追加一条配置记录,成功
B:读取另一个文件,返回"文件不存在"
模型找到正确路径后,可以重新发起 B。把 A、B 全部再执行一次,可能会重复追加配置。
因此,恢复前要查看各次调用的实际结果。对失败调用也不能只凭"报错了"就认定它毫无影响:读取失败与写入一半后失败,需要不同的处理方式。
如果业务要求两个修改一起成功,就需要额外设计,例如暂存修改并统一提交,或者为已经完成的操作提供补偿。是否能这样做,取决于工具背后的文件系统或业务服务,不能由并行调度自动保证。
取消不等于撤销
假设 A 已经修改文件,B 仍在执行,此时用户取消任务。A 的修改已经发生,发送取消请求不会让文件自动恢复。
Pi 会把取消信号传给工具,并在相关调度位置检查取消状态。正在执行的工具能否及时停止,还取决于具体实现是否处理这个信号,以及操作进行到了哪一步。
这里应分别看待"请求取消""确认停止"和"撤销修改"。例如,一个外部请求可能已经成功,只是响应还没回来;此时仅凭本地取消,无法确认远端操作也被撤销。
从结果反馈的设计上,应尽量说明哪些操作已完成、哪些已停止、哪些状态还不确定。只显示"任务已取消,文件没有变化",会掩盖已经发生的修改。
用两个小测试观察行为
本地运行了仓库已有的两个定向测试,使用模拟模型和简单的回显工具,不访问真实模型服务。两个测试均通过。
第一个测试让第一个调用等待,第二个调用先完成,再检查事件与结果消息。通过的断言对应以下顺序:
text
执行结束通知:第二个 → 第一个
工具结果消息:第一个 → 第二个
本轮结束携带的结果:第一个 → 第二个
这验证了完成顺序和结果顺序可以不同。测试检查的是实际事件和消息,而不仅是最终文本有没有出现。
第二个测试在前置钩子中阻断第一个调用,并设置终止标记;第二个调用正常执行。通过的断言表明:
text
实际执行的调用:只有第二个
模型请求次数:2 次
由于并非所有工具结果都要求终止,循环继续请求模型。这也说明某个调用的终止标记不能当作整批任务的取消开关。
这两个测试验证了排序与局部阻断。实际文件写入后的回滚、外部服务的取消与补偿,则需要针对具体工具另外验证。
理解 Pi 的工具并行,可以沿着一条顺序看:先判断操作是否独立,再看准备和执行如何安排,然后检查结果如何配对,最后确认部分失败或取消后留下了什么状态。这样才能判断并行究竟节省了哪些等待,又需要承担哪些恢复成本。
