本文目录
一句话:把「LLM + 一个工具循环」拼起来,就是 Agent 的本质。面试里「Agent 比直接调 LLM 多了什么」「ReAct 怎么跑」「Function Calling 怎么落地」,本质都在问这 30 行。
先建立心智:Agent = LLM + 工具循环
直接调一次 LLM,是「问一句、答一句」,模型只能用它脑子里的东西回答。
Agent 多了一个循环:模型可以说「我先去查一下」,你把它要查的工具真的执行,把结果喂回去,它再接着想——直到它觉得够了,给出最终答案。
用户问题
↓
┌─────────────────────────────┐
│ 调 LLM(带上「可用工具」清单) │ ←────────┐
└─────────────────────────────┘ │
↓ │
模型要调工具吗? │
├─ 要 → 执行工具 → 把结果加回对话 ─────────┘
└─ 不要 → 输出最终答案,结束这个「想 → 调工具 → 看结果 → 再想」的循环,就是 ReAct(Reasoning + Acting)最朴素的样子。
最小实现(可跑,约 30 行核心)
先完成第 2 篇的环境与凭据准备,并确认所选模型支持这里的 Chat Completions 工具调用格式。下面只注册一个工具:查天气。工具返回固定假数据,用于观察调用循环,不是实时天气。
import json
import os
from openai import OpenAI
client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"], timeout=30.0, max_retries=2)
MODEL = os.environ["LLM_MODEL"]
MAX_STEPS = 8
# 1) 真正干活的函数(模型只会「说要调」,执行是你的事)
def get_weather(city: str) -> str:
fake = {"北京": "晴 26℃", "上海": "多云 24℃"}
return fake.get(city, "暂无数据")
# 2) 把函数描述成模型能看懂的「工具 schema」
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询某个城市今天的天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名,如 北京"}},
"required": ["city"],
},
},
}]
def run(user_input: str):
messages = [{"role": "user", "content": user_input}]
for _ in range(MAX_STEPS): # 有界的Agent循环
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=tools,
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls: # 模型不调工具了 → 给最终答案,收工
return msg.content
for call in msg.tool_calls: # 模型要调工具:逐个执行
try:
if call.function.name != "get_weather":
raise ValueError("未注册的工具")
args = json.loads(call.function.arguments)
if not isinstance(args, dict) or set(args) != {"city"} or not isinstance(args["city"], str):
raise ValueError("city必须是字符串,且不能有额外参数")
result = get_weather(**args)
except (ValueError, TypeError) as error:
result = f"工具调用失败:{error}"
messages.append({ # 把结果按规范喂回去
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
return "达到工具调用轮数上限,已停止执行。请检查任务和调用记录。"
print(run("北京和上海今天哪个更暖和?"))一种可能的轨迹是:模型查询两座城市,收到演示数据,再比较温度。模型不一定按固定次数调用,也可能直接回答;应检查实际 tool_calls 和工具结果。这个最小 Agent 的核心,是带停止条件的“调用 → 执行 → 回灌”循环。
逐段拆解(面试能讲清的点)
- 工具 schema:模型不会执行代码,它只会根据
description决定「要不要调、传什么参数」,然后吐出一个结构化的调用请求。执行永远是你的代码做的。 role: "tool"回灌:工具结果必须按规范(带tool_call_id)加回messages,模型下一轮才能「看见」结果。漏了这步是新手最常见的 bug。- 循环出口:
msg.tool_calls为空就代表模型决定收尾。一定要给循环加最大轮数上限(下面坑里讲),否则可能死循环烧钱。 - 多工具:把更多函数加进
tools列表 + 在执行处按call.function.name分发,就从「单工具」变「工具箱」。
常见坑 + 工程取舍
- 不设轮数上限:模型可能反复调工具不收尾。示例已用
MAX_STEPS兜底;超限时明确停止。若还要调用模型做总结,也应单独限制请求预算。 - 工具报错不处理:工具抛异常、超时、返回空,要
try/except兜住,把「失败原因」也当作tool结果喂回去,让模型自己决定重试还是换路——而不是整个程序崩。 - 上下文膨胀:每轮都往
messages堆东西,长任务很快撑爆上下文。轻量做法是截断/摘要历史(再往上就是训练营讲的 ReSum 那套)。 - 权限与副作用:能「下单/删库/发消息」的工具,必须加确认或权限闸门——别让模型一句话就把生产数据删了。
关联面试题(去站内练手)
- 「ReAct 框架的工作流程是什么?和 Plan-and-Execute 有什么区别?」
- 「Function Calling 在 Agent 里是怎么落地的?数据怎么造?」
- 「Agent 比直接调 LLM 多了什么?」
👉 去 题库 搜 Agent / Function Calling / ReAct,用相关题目继续练:先讲清这段最小实现,再说明状态、失败处理和生产边界。
训练营延伸
这 30 行是「单 Agent」的地基。再往上——多 Agent 协作、统一工具网关 + 沙箱、长程记忆压缩、用 RL 训练 Agent 的决策策略、可观测与私有化上线——就是训练营里集群 Agent 那条线在做的事。