代理

一个**代理**是在循环中运行的模型,逐次选择要调用的工具,直到任务完成。本章的其余内容解释了这意味着什么、它的优势以及隐含的代价。
代理循环
在工具使用中,模型调用了一个函数,你将结果发回。但有些任务需要多个步骤,而你事先不知道这些步骤是什么。"找到最便宜的航班并预订它"可能意味着搜索、比较、检查日历,然后预订,模型必须在进行过程中找出这个顺序。
所以与其处理单个工具调用,你把整个流程放在一个循环中。这个重复的周期就是**代理循环**,一个重复的回合,直到任务完成:
- 将对话和可用工具发送给模型。
- 模型要么调用工具,要么给出最终答案。
- 如果它调用了工具,运行它,将结果添加到对话中,然后回到步骤 1。
- 如果它给出了最终答案,停止。
每回合,模型都能看到到目前为止发生的一切,包括之前的工具结果,并决定下一步行动。这是与工具使用的唯一区别:你持续循环,直到模型确信它已经完成。
真正的转变在于谁在掌舵。在本章之前的所有章节中,你的代码决定了控制流:你选择何时调用模型、何时运行工具、接下来做什么。代理将这个决定权交给了模型。循环不断询问"接下来应该发生什么?",而模型的预测就是每次的答案。你用灵活性换取了控制权,而这种权衡既是优势也是风险的来源。
在底层,没有什么新发生。它是同样的预测,只是背景在增长,工具使用的来回过程不断重复。模型不会制定计划并执行它。每一回合,它都从现在能看到的东西中预测最好的下一步,然后循环再次运行。
当这个有效时,它看起来像是朝着目标推理。当它无效时,代理会自我循环或走入死胡同,因为系统的任何部分都不在掌握整体计划。
最小代理
这是代码中的循环。它重用工具使用中的工具定义和实现,并将它们包装在循环中,直到模型停止要求工具。
import json
def run_agent(user_text, tools, tool_impls, max_steps=6):
messages = [{"role": "user", "content": user_text}]
step = 0
while step < max_steps:
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message)
calls = message.tool_calls
if not calls:
return message.content # 没有工具请求:这是最终答案
# 运行每个请求的工具并将结果反馈回来
for call in calls:
args = json.loads(call.function.arguments)
result = tool_impls[call.function.name](args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
step += 1
return "已停止:代理达到了其步骤限制。" # 安全网从头到尾读:调用模型,将其消息推送到对话中,如果它要求了工具,运行所有工具并附加结果,然后再循环。当模型返回文本而不是工具调用时,那就是最终答案,你返回它。
max_steps值设置一个**步骤限制**,它是防止循环失控的安全网,这是下一部分的主题。这里的形状与一个提供者的 SDK 匹配;字段名在提供者之间不同,但循环在任何地方都是相同的。
停止和防护措施
一个让模型决定何时停止的循环可能无法停止。它可能永远调用工具、重复自己,或追逐死胡同。因为每一步都是一个按代币计费的模型调用,失控的代理会快速烧钱和时间。
所以**防护措施**,你在循环周围放置的限制,是设计的一部分,而不是你之后添加的抛光步骤:
- 一个步骤限制。 限制循环数量,
max_steps的方式。当达到时,停止并返回。 - 验证工具输入。 模型选择参数,所以在运行任何有真实后果的东西之前检查它们。
- 限制工具能做什么。 给代理它需要的最少权力。一个读取的工具比一个删除的工具更安全。
- 观察成本。 每个任务的多个模型调用快速相乘,所以追踪代币并设置预算。
将这些联系在一起的线索:模型在即兴进行路径,所以你放置了它不会的限制。假设代理至少会做一次错误的事情,并使其可生存。
何时工作流胜过代理
代理很令人兴奋,这使得当你不需要时很容易去取一个。纯规则:如果你已经知道步骤,不要使用代理。将它们写出来作为纯代码,一个**工作流**,每个步骤以固定顺序运行。
一个工作流更可靠、更便宜,而且远更容易调试,因为你控制路径而不是希望模型找到它。为开放结束的任务保存代理,其中步骤取决于沿途发现的东西。
许多看起来需要代理的功能最好是作为结构化输出调用或RAG查找,由手工连接。只在你不能事先写下步骤时才取代理。
实际应用
上面的run_agent函数就是全部。给它一个问题、一个工具集和它们的实现,它循环直到完成或直到它击中步骤限制。一个小任务展示了循环值得其保持:
answer = run_agent(
"北京的天气怎样,我应该带伞吗?",
tools, # 包括来自工具使用章节的 get_weather
tool_impls, # {"get_weather": ...}
)
# 代理调用 get_weather,读"多云",然后在一个最终回合给出答案:
# "北京多云,温度 18 度,所以带伞是个明智的想法。"一个工具、一个循环、一个答案。相同的结构规模到多个工具和多个步骤而不改变形状。一旦你可以构建这么有能力的东西,下一个问题是你如何知道它们实际有效,这是evals的内容。
run_agent循环是全部模式:给它一个问题、工具和它们的实现,它循环直到模型停止要求工具或击中限制。一个工具和一个循环规模到许多而不改变形状。下一个问题是你如何知道它实际有效,那是 evals。 
