Agent:规划、工具调用与记忆
Agent = LLM + 工具 + 循环。
它和普通对话的区别只有一个:模型可以采取行动,并根据结果继续决策。
一、最小 Agent 就是一个 while 循环
模型决策说话,还是调工具?
执行工具按参数调用,拿到结果
结果回填把结果放回消息列表
再决策基于新信息继续或收尾
循环直到任务完成或达到步数上限
python
def run_agent(question, tools, max_steps=8):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question},
]
for step in range(max_steps):
resp = call_llm(messages, tools=tools) # 模型决定:说话 or 调工具
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls: # 没有工具调用 = 结束
return msg.content
for call in msg.tool_calls:
result = execute(call.function.name, call.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
})
return "已达到最大步数,未能完成任务。"记住这个循环。 所有框架(LangGraph、AutoGen、CrewAI)都是它的工程化封装。
二、工具设计:Agent 成败的 80%
一句最重要的经验
Agent 不可靠,几乎总是因为工具设计得差,而不是模型不够强。
| 原则 | 反例 | 正例 |
|---|---|---|
| 名字自解释 | query | search_orders_by_phone |
| 描述写清何时用 | 「查询数据」 | 「按手机号查订单。用户提供手机号后用此工具;不要传姓名」 |
| 参数少而明确 | params: object | phone: string, 11 位手机号 |
| 返回精简 | 返回 200 条原始记录 | 返回 ≤10 条关键字段 + total |
| 错误可读 | Error 500 | 未找到该手机号的订单,请确认号码或改用订单号查询 |
| 幂等安全 | 每次调用都创建订单 | 查询类默认安全;写操作需确认 |
python
TOOLS = [{
"type": "function",
"function": {
"name": "search_orders_by_phone",
"description": "按手机号查询该用户的历史订单。当用户询问自己的订单状态、物流、退款进度时使用。",
"parameters": {
"type": "object",
"properties": {
"phone": {"type": "string", "description": "11 位中国手机号,不含空格"},
"limit": {"type": "integer", "description": "返回条数,默认 5,最大 10"},
},
"required": ["phone"],
},
},
}]工具设计的五条硬规则
- 一个工具只做一件事(不要
handle_everything) - 描述里写「什么时候用」比「是什么」更重要
- 返回结果要控制在几百 token 内 —— 否则撑爆上下文且干扰判断
- 写操作必须可确认/可撤销,或强制人工审批
- 给「查不到」明确的返回值,而不是抛异常让模型猜
三、四种 Agent 形态
| 形态 | 说明 | 适用 | 风险 |
|---|---|---|---|
| 单次工具调用 | 判断要不要调、调哪个、答 | 80% 场景 | 极低 |
| ReAct 循环 | 推理→行动→观察,多步 | 多步查询、需要组合 | 中 |
| 规划-执行 | 先出计划,再逐步执行 | 复杂任务、步骤长 | 中高 |
| 多智能体 | 多个角色分工协作 | 极复杂、可并行 | 高(调试困难) |
强烈建议:从最简单的开始
能用「意图路由 + 单次工具调用」解决的,不要上 Agent。 能用「固定的三步流水线」解决的,不要上自主循环。 自主性每增加一级,可靠性和可调试性就下降一档。
四、规划的实用做法
python
PLAN_PROMPT = """你是任务规划器。把用户需求拆成可执行的步骤。
可用工具:
{tool_list}
要求:
1. 每步只做一件事,可独立验证
2. 最多 6 步
3. 只输出 JSON 数组:[{"step":1,"action":"工具名或think","args":{...},"why":"目的"}]
4. 若无需工具即可回答,返回 []
用户需求:{question}"""关键实践:计划产物必须是结构化 JSON,而不是自然语言。这样才能逐步校验、部分重试、插入人工确认点。
循环控制(防失控)
| 机制 | 做法 |
|---|---|
| 最大步数 | 8–12 步封顶 |
| 重复检测 | 相同工具+参数连续两次 → 中断并提示 |
| 预算控制 | token/金额上限 |
| 超时 | 总时长上限(如 60s) |
| 死循环识别 | 无新信息产出即终止 |
| 人工检查点 | 关键写操作前暂停确认 |
五、记忆:三种,别混用
| 类型 | 存什么 | 实现 |
|---|---|---|
| 短期(会话) | 最近 N 轮对话 | 消息数组,超长则摘要 |
| 长期(事实) | 用户偏好、历史结论 | 向量库/结构化存储,检索后注入 |
| 工作记忆 | 当前任务的中间结果 | 显式变量(如 state["found_orders"]) |
python
def compress_history(messages, keep_last=6, max_chars=4000):
"""长会话压缩:保留首轮(含系统约束)+ 最近若干轮 + 早期摘要。"""
if sum(len(m.get("content") or "") for m in messages) < max_chars:
return messages
early, recent = messages[1:-keep_last], messages[-keep_last:]
summary = call_llm("用要点总结以下对话的关键事实与结论:\n" +
"\n".join(f"{m['role']}: {m.get('content','')}" for m in early))
return [messages[0], {"role": "system", "content": f"早前对话摘要:{summary}"}] + recent记忆不是越多越好
塞入无关历史会让模型跑偏,还会显著增加成本。记忆的价值在于「相关」,不在于「全」。
六、可靠的 Agent 工程要求
| 要求 | 做法 |
|---|---|
| 可观测 | 每步记录:输入、决策、工具、结果、耗时、token |
| 可重放 | 保存完整 trace,能复现失败案例 |
| 可中断 | 长任务支持取消、断点续跑 |
| 幂等 | 写操作带 request_id,重试不重复下单 |
| 降级 | 工具失败时能退回「只回答不操作」 |
| 限权 | 工具集最小化,敏感操作走审批 |
| 沙箱 | 代码执行类工具必须隔离环境 |
七、评测 Agent
| 指标 | 定义 |
|---|---|
| 任务成功率 | 端到端完成的比例(最重要) |
| 平均步数 | 越少越好(成本/延迟) |
| 工具调用准确率 | 选对工具+参数的比例 |
| 无效调用率 | 重复/多余调用 |
| 平均成本/延迟 | 每任务 |
| 危险行为率 | 越权、循环、误写 |
评测集要包含:正常多步任务、需要澄清的模糊任务、工具会失败的任务、诱导越权的任务。
八、常见失败模式与对应
| 失败 | 原因 | 处理 |
|---|---|---|
| 反复调同一个工具 | 返回没给有效信息 | 改进返回内容/加「已查询过」提示 |
| 不调用工具就编答案 | 描述不清晰 / 系统提示没强制 | 明确「涉及个人数据必须先查」 |
| 参数格式错 | 参数定义含糊 | 加示例与枚举,服务端做校验并返回可读错误 |
| 步数爆炸 | 任务拆得太细 | 合并工具、给更强的规划约束 |
| 越权操作 | 工具权限过大 | 最小权限 + 人工确认 |
| 上下文被工具结果撑爆 | 返回太啰嗦 | 服务端截断 + 摘要字段 |
通过标准
下一步
→ 评测与可观测