从“问答”到“行动”:Agent 是什么
普通的对话式模型只能“说”,Agent 的目标是“做”。它把大模型放进一个循环:观察当前状态 → 决定下一步动作 → 调用工具执行 → 把结果放回上下文 → 继续判断,直到任务完成。
这个循环带来两个根本变化。第一,模型获得了与外部世界交互的能力:查数据库、调接口、写文件、发消息。第二,任务的完成不再依赖一次生成,而是依赖多步决策——这既提升了上限,也引入了新的失败模式:死循环、工具误用、目标漂移。
Agent 的本质不是更聪明的模型,而是把“决策”与“执行”解耦后形成的闭环系统。
理解这一点后你会发现,构建 Agent 的难点很少在模型本身,而在循环控制、工具设计与错误处理。
ReAct:推理与行动交替的核心循环
ReAct(Reasoning + Acting)是最经典也最实用的 Agent 范式。它让模型在每一步先输出一段思考(Reasoning),再输出一个动作(Action)及其参数,系统执行动作后把观察结果(Observation)追加回上下文,模型据此继续。
这种交错结构的价值在于用外部反馈校准内部推理。模型不需要一次猜对所有事,它可以先试探、看结果、再修正。相比“一次性给答案”,多步交互能处理信息不完整、需要试错的任务。
实践中,思考部分建议限制长度(比如 100 字以内),并要求模型用固定关键字标记动作(如 Action: search、Action Input: {...})。这样系统可以用简单解析器稳定地取出动作,而不是依赖脆弱的自然语言理解。
工具调用:把函数暴露给模型
工具是 Agent 的手脚。一个工具就是一段可以被模型调用的函数:名称、用途描述、参数结构、返回值。设计工具时有三条经验。
- 描述即文档模型的选型完全依赖工具描述,写清“什么时候用、什么时候不要用”。
- 粒度适中太粗一件事做不完,太细要调十几次;一个工具对应一个完整语义动作。
- 错误友好返回结构化错误信息(原因 + 建议),让模型有机会自我修正而不是直接崩掉。
另一个常被忽略的点是工具数量的上限。当你有几十个工具时,模型的选择准确率会明显下降。解决方案是按场景做工具子集、引入检索式的工具路由,或者把多个相关能力合并成一个带 mode 参数的工具。
工具定义的写法各平台略有差异,但结构是一致的:一个 JSON Schema 描述参数,一个本地函数负责执行。下面这段代码不依赖任何第三方库,可以直接运行,演示“模型给的参数 → 本地工具执行 → 结构化结果回填”的完整链路:
import json
# 1) 暴露给模型的工具描述(真实项目里直接提交给工具调用接口)
TOOLS = [{
"name": "get_weather",
"description": "查询指定城市当前天气。仅当用户明确询问天气时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如 佛山"},
"unit": {"type": "string", "enum": ["c", "f"], "default": "c"}
},
"required": ["city"]
}
}]
# 2) 本地实现:成功返回 dict,失败返回结构化错误,便于模型自我修正
def get_weather(city, unit="c"):
fake_db = {"佛山": 28, "北京": 22}
if city not in fake_db:
return {"ok": False, "error": "CITY_NOT_FOUND", "hint": "请确认城市名,或改用附近城市"}
t = fake_db[city]
if unit == "f":
t = round(t * 9 / 5 + 32)
return {"ok": True, "city": city, "temperature": t, "unit": unit}
# 3) 调度器:校验参数 -> 执行 -> 统一回填
REGISTRY = {"get_weather": get_weather}
def dispatch(tool_call):
name = tool_call.get("name")
args = tool_call.get("arguments", {})
fn = REGISTRY.get(name)
if fn is None:
return {"ok": False, "error": "UNKNOWN_TOOL", "hint": "可用工具:%s" % list(REGISTRY)}
try:
return fn(**args)
except TypeError as e:
return {"ok": False, "error": "BAD_ARGUMENTS", "hint": str(e)}
if __name__ == "__main__":
calls = [
{"name": "get_weather", "arguments": {"city": "佛山"}},
{"name": "get_weather", "arguments": {"city": "佛山", "unit": "f"}},
{"name": "get_weather", "arguments": {"city": "火星"}},
{"name": "get_time", "arguments": {}},
]
for c in calls:
print(c["name"], "->", json.dumps(dispatch(c), ensure_ascii=False))
把它跑起来会看到四种结果:正常返回、单位换算、城市不存在、工具不存在。最后两种正是 Agent 能否自我修正的关键——错误信息足够结构化,模型才有机会换个城市或换工具重试,而不是把异常抛给用户。
记忆:短期上下文与长期存储
记忆分两层。短期记忆就是上下文窗口:把最近几轮的对话、工具返回值按顺序保留。它的限制很明确——装不下、会变慢、越靠中间越容易被忽略。因此常见的做法是滑动窗口加摘要:超出长度的部分压缩成一段要点摘要,放在对话开头。
长期记忆需要外部存储:向量库存语义片段,关系库存结构化事实(用户偏好、项目配置、历史结论)。写入时机很关键——不是每句话都存,而是抽取稳定、可复用的信息。读取时也要控制数量,检索回来的内容要做重排序,只把最相关的几条塞进上下文。
一个实用原则:能放进工具结果的东西,不要塞进记忆。记忆应该是“跨会话的稳定知识”,而不是“这次任务的临时数据”。
规划与反思:让 Agent 少走弯路
规划让 Agent 在动手前先想清楚路径。最简单的形式是让模型先输出一个待办列表,然后逐步执行并在完成后打勾。这比让模型边走边看更稳定,因为任务被显式分解后,遗漏步骤的概率大幅下降。
反思(Reflection)是另一半。每完成一个子任务,让模型检查结果是否满足目标:数据是否完整、格式是否正确、有没有与已有信息矛盾。发现问题就让循环继续,而不是直接结束。注意反思要设置次数上限,否则容易出现“反复自我否定、永不停机”的情况。
如果任务足够复杂,可以采用“先规划、再执行、最后校验”的三段式:规划阶段只产出步骤和判据,执行阶段只做单步子任务,校验阶段对照判据逐项检查。这三段甚至可以用不同的提示词与不同的模型——用便宜模型做执行、用强模型做规划与校验,能显著压低成本。
多智能体协作:分工与通信
多智能体不是噱头,它的价值在上下文隔离。当单个 Agent 需要同时掌握大量彼此无关的信息时,上下文会被稀释,决策质量下降。把任务拆给多个各司其职的 Agent,每个只看到自己关心的部分,效果往往更好。
常见分工模式有三种:“经理 + 工人”,一个负责拆解与汇总,其余负责执行;“流水线”,各 Agent 按顺序处理同一份数据的不同阶段;“辩论”,多个 Agent 独立作答再由一个仲裁者比较。工程上最稳的是第一种,最容易失控的是第三种。
通信协议要简单:统一的消息结构(谁、做什么、结果、状态),明确的任务边界,以及一个全局的终止条件。缺少终止条件是多智能体系统失控的首要原因。
用 Python 实现最小 ReAct Agent
下面用一个几十行的循环实现最小 ReAct Agent,工具只有两个:计算器和关键词检索(用本地字典模拟)。重点看循环控制与解析逻辑。
import re
TOOLS = {
# 演示用;生产环境请替换为安全的表达式求值库
"calc": lambda expr: str(eval(expr, {"__builtins__": {}}, {})),
"kb": lambda q: {"agent": "Agent 是能调用工具、多步决策的系统。",
"react": "ReAct 让模型交替输出思考与动作。"}.get(q.strip().lower(), "未找到相关条目"),
}
def run_agent(question, call_model, max_steps=6):
scratchpad = ""
for step in range(max_steps):
prompt = (f"问题:{question}\n可用工具:calc(表达式), kb(关键词)\n"
f"历史:\n{scratchpad}\n"
"请先写 Thought:,再写 Action: 工具名,最后写 Action Input: 参数。"
"若已能回答,写 Final Answer: 结论。")
out = call_model(prompt)
if "Final Answer:" in out:
return out.split("Final Answer:", 1)[1].strip()
m = re.search(r"Action:\s*(\w+)\s*Action Input:\s*(.+)", out, re.S)
if not m:
scratchpad += "\n[系统] 格式无法解析,请按 Action/Action Input 重新输出。"
continue
name, arg = m.group(1).strip(), m.group(2).strip()
if name not in TOOLS:
obs = f"错误:未知工具 {name},可用工具为 {list(TOOLS)}"
else:
try:
obs = TOOLS[name](arg)
except Exception as e:
obs = f"错误:{type(e).__name__} {e},请修正参数后重试"
scratchpad += f"\nThought/Action: {out}\nObservation: {obs}"
return "已达到最大步数仍未得出答案,请提供更明确的问题。"
# call_model 替换为真实 API,例如 lambda p: client.chat(p)
# print(run_agent("Agent 和 ReAct 分别是什么?", call_model))
这段代码体现了三个关键设计:步数上限防止死循环;解析失败时把错误写回上下文让模型自我纠正;工具异常被捕获成文本观察而不是让程序崩溃。把这三点补上,Agent 的稳定性会有质的提升。
想进一步验证,可以把 kb 换成真实的向量检索,把 calc 换成安全的表达式求值库,并加上每次调用的耗时与 Token 统计。
工程落地:可控性、成本与安全边界
把 Agent 放进生产环境,需要额外四道保障。
- 权限最小化:给 Agent 的工具凭据只开放必要的读写范围,危险操作(删除、支付、外发消息)必须二次确认。
- 成本熔断:为每次任务设置最大步数、最大 Token 和最长耗时,超限即中止并返回中间结果。
- 可观测性:记录每一步的输入、动作、参数、观察和耗时,出问题时能还原完整轨迹。
- 人工兜底:明确哪些任务必须人工复核,尤其是对外发布的输出。
一个能干的 Agent 和一个危险的 Agent,区别往往不在模型能力,而在有没有边界。
下一篇转向基础:如果你还没有系统学过机器学习,可以从监督学习与经典算法速览开始,把评估指标和过拟合这些概念先建立起来。