夜雨聆风学习资料网

ARTICLE · 1098524

解构Pi源码学习Agent系列(二)Agent Loop的核心原理

解构Pi源码学习Agent系列(二)Agent Loop的核心原理

上一篇文章我们了解了Pi的整体架构,以及各个包之间的关系。今天我们来看一下Pi的Agent Loop的核心,Agent是如何与大模型交互的,大模型又如何使用工具的,这篇文章就来重点拆解,源码文件在packages/agent/src/agent-loop.ts。

当我让Pi修复登陆页面的问题,并且执行测试时,它的步骤可能是,先读文件找到问题,再修改代码,然后执行测试。如果测试失败,它还会继续修改。我们只用了一个指令,它为什么可以执行这么多步骤?工具执行完了之后,又是怎么让模型继续执行任务的?

讲解之前我们先回顾一下ReAct的原理,它的名字来自Reasoning Action,即思考和行动。 

我们可以用三个环节来理解这个过程:

  •     Reasoning(推理):根据当前信息判断下一步需要做什么。

  •     Action(行动):调用工具,与外部环境交互。

  •     Observation(观察):获得行动结果,把新的信息用于后续判断。

比如我们有一个登陆页面不能跳转到主页的问题,Agnt会先调用read工具读取文件里面的代码,然后定位代码的问题调用edit工具修改代码,修改完了之后调用bash工具执行测试,如果测试失败,它还会继续修改。就是这样不断的循环,直到问题解决。这就是ReAct的执行原理,Pi的Agent Loop也是基于这个原理实现的。

开始任务,把消息准备好

我们先理解几个概念:

  • 一次运行:是代表整个循环过程,从agent_start到agent_end。

  • 一个回合:是代表一次模型生成的内容,以及模型对工具的调用。

  • 一个工具批次:是指模型对工具的全部调用,模型一次可以读取多个文件,这些调用属于同一个批次,执行方式由调度配置决定。

  • Follow-up:是指模型生成的内容的时候,用户可能会有新的需求,比如继续修改代码,或者继续执行测试,这些需求会在后续的回合中继续执行。

agentLoop和agentLoopContinue的区别是,agentLoop是开始一个新的任务,agentLoopContinue是继续执行一个任务。agentLoop就像新的会话框,带着问题“修复登录页面跳转失败的问题,并运行测试。”,agentLoopContinue就像已经打开的会话框,带着上下文执行当前任务。

核心流程的入口有两个函数,agentLoop和agentLoopContinue,两个入口都会进入runLoop走同一套流程。Pi先看用户执行任务的时候,中途补有没有要补充的指示,也就是Steering(插话)。

如果有插话就暂存在pendingMessages,等待加入上下文。然后让hasMoreToolCalls为true,开始第一次请求模型。如果后续有输入再次请求模型,会先经过prepareNextTurn,更新需要使用的上下文和配置。如果没有待处理消息,准备完成后还会再取一次插话,作为准备期间新的输入。如果有处理的消息,就把消息加入上下文,没有就继续使用已有消息,然后请求模型。

模型返回后,看看要不要用工具

调用模型的时候,模型可能直接回答,也可能要求调用工具,例如:“先读取登录页面的代码。”Pi先检查模型是否出错了,如果stopReason为error或aborted,就结束当前回合和本次运行。没有出错就再看响应里有没有toolCall。

  •     没有工具调用:结束这一回合,进入后面的判断。

  •     有工具调用:检查能否执行,再交给工具调度。

模型的输出是有上下文限制的,如果模型输出的内容超出了上下文限制,Pi会拒绝执行这一响应中的工具调用,并生成错误结果,告诉模型再重新执行。执行过程没有被阻断时,进入executeToolCalls,按配置串行或并行处理工具调用。

工具结果放回上下文

模型调用工具read返回了登录页面的代码,Pi会把结果保存为toolResult,加入上下文。下一次请求模型时,大模型可以根据这些代码决定如何修改。修改工具的结果和测试工具的结果,也按同样的方式交回模型。

执行的过程如下:

模型要求读文件 → read 返回代码模型根据代码要求修改 → edit 返回修改结果模型要求运行测试 → bash 返回测试结果模型根据测试结果继续修改,或生成汇报

每次大模型的请求和它对应的工具调用组成一个“回合”,然后工具把调用的结果给模型之后,会进入下一回合。

每个回合结束后,判断还要不要继续

Pi 发出 turn_end,表示这一回合就结束了,并保存这一回合信息。接着检查 shouldStopAfterTurn(),如果上层要求停止,就直接结束本次运行。否则取出新到的 Steering(插话),再判断是否继续内层循环。普通工具调用完成后,还需要让模型处理结果,所以会再走一轮。即使模型没有调用工具,只要有新插话,也能继续。工具批次还可以提示停止,具体判断由工具返回的结果决定,待处理消息仍可能推动下一回合。

比如,用户补充:“后续只修改登录页面,不要动公共路由。”Pi 会取出这条指示,加入下一次模型请求。它不会直接打断正在执行的工具,同一响应中的工具调用仍会继续处理。

图中的蓝色框就是这个反复推进的内层循环,它检查工具是否需要继续,以及有没有待处理消息。

内层结束,再看看有没有追加任务

内层没有继续条件时,Pi 还会检查一次 Follow-up(追加任务)。例如,用户又加了一条消息:“修复后,再整理一份变更说明。”这条消息会在内层结束后取出。有追加消息,就保存到 pendingMessages,重新进入内层循环,用已有上下文继续处理。没有追加消息,就发出 agent_end,结束本次运行。图中紫色框就是外层循环,内层暂时结束后,它负责接接收追加任务。

从整体来看,就是:

请求模型,读取代码根据代码修改根据修改结果运行测试根据测试结果继续处理或汇报没有工具推进和插话,退出内层有追加任务,继续处理变更说明再次退出内层,也没有追加任务agent_end,结束运行

turn_end表示一轮结束,后面还可以继续,agent_end表示本次运行结束。从整个过程来看,内层循环结束的条件是,hasMoreToolCalls为 false,没有工具调用要求,或者工具批次给出了终止提示。pendingMessages.length为 0,没有等待处理的新消息。外层循环结束的条件是,没有追加任务。

用让同事帮忙带饭的例子理解整个过程

中午,同事准备下楼买饭,你对他说:“帮我带一份鸡腿饭回来。”同事先想好下一步做什么,再实际行动,最后根据得到的结果决定接下来怎么办。

决定去常去的餐厅买饭 → 到店询问 → 得知鸡腿饭卖完了决定问你要不要换一种 → 发消息询问 → 你说牛肉饭也可以决定买牛肉饭 → 付款取餐 → 拿到午饭决定带回办公室 → 回到公司把饭交给你 → 确认你已经收到这件事告一段落

这里,想好下一步要做什么对应请求模型,“询问、买饭、带回办公室”对应执行工具,“鸡腿饭卖完了、你同意换牛肉饭、已经拿到饭”对应工具返回的结果。每次决定和对应的行动组成一个回合,行动结果会帮助同事决定下一步要做什么。

内层循环就像一步步完成这次带饭的事情,到了餐厅,要确认能买什么。买到饭后,要带回办公室。交给你后,再告诉你结果,每一步得到的信息都会影响下一步的安排。

同事在等待牛肉饭的时候,你又发来一条消息:“记得不要放辣椒。”这就像 Steering(插话),同事在下一次决定怎么做时,把这个新要求考虑进去,已经完成的动作不会因为这条消息撤销。

等同事告诉你饭已经带到,不再需要安排新的行动,也没有需要回应的补充要求(Steering),这次带饭的内层循环就结束了。

外层循环就像办完眼前的事,再看看有没有排在后面的请求。 假设你又说:“饭带回来后,再帮我去楼下取一下快递。”这就像 Follow-up(追加任务)。同事先处理带饭这件事,等内层循环结束,再取出“拿快递”的请求,进入新的内层循环,帮你取快递。

快递也拿回来了,也没有待处理的插话,内层再次结束。此时如果没有其他追加请求外层也结束,这次帮忙就告一段落。

总结

以上就是Agent Loop的核心流程,整体看下来是不是也没有很复杂,就是根据模型返回的内容,反复执行工具调用,直到任务完成。下一篇我们会从创建一个最简单的 Agent 开始,走进agent.ts看看模型、工具、消息历史和运行状态如何保存,以及它们怎样交给Agent Loop使用。

相关学习资料