一句话定位:解决 Agent "健忘"、"死循环"、"瞎想"三大顽疾。读完能给 Agent 添加记忆和规划能力。
1. 引言:Agent 的三大顽疾
上一篇中,我们构建了一个最小可运行的 Agent,它能自主调用工具来诊断问题。但如果你把它投入实际使用,很快会遇到三个令人抓狂的场景:
顽疾一:健忘(重复劳动)
plain
用户:帮我查一下订单服务最近的情况。
Agent:
→ 调用 query_logs → 返回:"大量数据库连接超时"
→ 调用 query_metrics → 返回:"错误率 15%"
用户:那这些错误是什么时候开始的?
Agent:
→ 调用 query_logs → 返回:"大量数据库连接超时" ← 又查了一遍同样的日志
→ 分析结果,回答用户
Agent 在第二轮对话中完全忘记了第一轮已经查过日志,白白多调用了一次 API,浪费了 Token 和时间。
顽疾二:死循环(盲目重试)
plain
Agent 调用修复工具 → 返回:"权限不足,无法执行"
Agent 再次调用修复工具 → 返回:"权限不足,无法执行"
Agent 再次调用修复工具 → 返回:"权限不足,无法执行"
...(循环 10 次,消耗 50000 Token,直到触发 max_iterations 限制)
Agent 收到"权限不足"后,没有分析错误原因、调整策略,而是机械地重试同一个操作------直到 Token 耗尽或触发安全限制。
顽疾三:瞎想(偏离目标)
plain
用户:帮我修复订单服务的数据库连接问题。
Agent:
→ 查询日志 → 发现数据库连接超时
→ 开始查看数据库的版本信息
→ 查看数据库的配置参数
→ 查看数据库的存储引擎
→ 查看数据库的索引情况
→ 查看数据库的字符集设置
→ ...(越走越远,始终没有去解决连接超时的问题)
Agent 没有一个清晰的"计划",每一步都凭直觉决定做什么,结果在大量不相关的排查中迷路了。
这三个问题的根因是相同的:Agent 缺乏有效的记忆和规划机制。
- 健忘 → 缺少记忆管理(不知道自己之前做过什么)
- 死循环 → 缺少错误分析与策略调整(不会从失败中学习)
- 瞎想 → 缺少任务规划(不知道该按什么顺序做什么)
本篇逐一解决这三个问题。
2. Agent 的记忆机制
类比人类的认知系统,Agent 的记忆可以分为三种类型[1]:
latex
┌─────────────────────────────────────────────────────────────┐
│ Agent 记忆架构 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 工作记忆(Working Memory) │ │
│ │ "我现在正在想什么" │ │
│ │ 对应:LLM 的上下文窗口(Context Window) │ │
│ │ 生命周期:单次任务执行期间 │ │
│ │ 容量:受 Token 窗口限制(如 128K tokens) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 长期记忆(Long-term Memory) │ │
│ │ "我以前遇到过什么" │ │
│ │ 对应:外部存储(向量数据库 / 关系数据库 / 文件) │ │
│ │ 生命周期:跨任务、跨会话持久存在 │ │
│ │ 容量:理论上无限(取决于存储基础设施) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 工作台记忆(Scratchpad) │ │
│ │ "我当前做到哪一步了,中间结果是什么" │ │
│ │ 对应:结构化的中间状态记录 │ │
│ │ 生命周期:单次任务执行期间 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.1 工作记忆:上下文窗口管理
工作记忆就是 LLM 的上下文窗口 ------在每一轮推理中,LLM 能"看到"的全部信息。它包括系统 Prompt、用户输入、对话历史、工具调用结果等所有消息[2]。
上一篇的最小 Agent 中,messages 列表就是工作记忆:
python
messages = [
{"role": "system", "content": "你是一个 DevOps 工程师..."}, # 系统指令
{"role": "user", "content": "订单服务报错了"}, # 用户输入
# ... 工具调用和结果会不断追加 ...
]
问题:上下文窗口有上限。当 Agent 执行多步任务时,messages 会快速增长:
plain
第 1 步:[系统指令, 用户输入, LLM 推理, 工具调用, 工具结果]
≈ 500 tokens
第 5 步:[系统指令, 用户输入, (LLM推理 + 工具调用 + 工具结果) × 5]
≈ 2500 tokens
第 20 步:[系统指令, 用户输入, (LLM推理 + 工具调用 + 工具结果) × 20]
≈ 10000 tokens
第 50 步:[系统指令, 用户输入, (LLM推理 + 工具调用 + 工具结果) × 50]
≈ 25000 tokens
第 200 步(长流程任务):
≈ 100000 tokens → 可能超出窗口限制
当消息量接近或超出上下文窗口限制时,需要上下文管理策略 [2]:
策略一:滑动窗口(Sliding Window)
只保留最近 N 条消息,丢弃更早的消息。简单粗暴,但可能丢失重要信息。
latex
实现逻辑:
if len(messages) > MAX_MESSAGES:
messages = messages[:SYSTEM_PROMPT] + messages[-MAX_MESSAGES:]
# 保留系统 Prompt + 最近 N 条消息
策略二:摘要压缩(Summarization)
当消息量超过阈值时,用 LLM 将早期消息压缩为摘要,替换原始消息。
latex
实现逻辑:
if total_tokens > TOKEN_THRESHOLD:
old_messages = messages[1:-RECENT_COUNT] # 取出早期消息
summary = llm.summarize(old_messages) # 用 LLM 生成摘要
messages = [system_prompt] + [summary] + messages[-RECENT_COUNT:]
# 系统 Prompt + 摘要 + 最近 N 条消息
策略三:重要性过滤(Importance Filtering)
不是按时间淘汰,而是按信息重要性淘汰。不重要的消息先丢弃,重要的消息保留更久。
latex
重要性判断标准:
- 工具调用的返回结果 > LLM 的中间推理(结果比思考过程重要)
- 错误信息 > 成功信息(错误更值得记住)
- 用户的明确指令 > Agent 的自行判断
- 最终结论 > 过程细节
实际工程中的常见做法 是组合使用:先按重要性过滤,再对剩余消息做摘要压缩,最后用滑动窗口兜底[2]。
2.2 长期记忆:跨会话的持久化
工作记忆只在单次任务执行期间有效。任务结束后,messages 列表被清空,Agent 什么都不记得了。
下一次用户再来问:"上次那个数据库连接问题后来怎么解决的?"------Agent 完全不知道。
长期记忆 解决的是跨任务、跨会话的信息持久化问题[3]。
latex
┌─────────────────────────────────────────────────────────────┐
│ │
│ 工作记忆 vs 长期记忆 │
│ │
│ 工作记忆(上下文窗口) 长期记忆(外部存储) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 本次任务的信息 │ │ 历史任务的结论 │ │
│ │ 当前对话的上下文 │ │ 之前的诊断方案 │ │
│ │ 最近的工具调用结果 │ │ 用户偏好 │ │
│ │ │ │ 系统架构知识 │ │
│ │ 关机即失 │ │ 持久保存 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ 类比: 类比: │
│ 工作记忆 = 你正在写的草稿纸 长期记忆 = 你的笔记本 │
│ 写得快,但容量有限 容量大,但需要翻找 │
│ │
└─────────────────────────────────────────────────────────────┘
长期记忆的存储内容通常包括:
latex
情景记忆(Episodic Memory):
"2026-03-15 订单服务数据库连接超时,根因是锁争用,
解决方案是 kill 阻塞 SQL [ID: 8837],耗时 8 分钟"
→ 记录"发生过什么"和"怎么解决的"
语义记忆(Semantic Memory):
"订单服务的数据库实例是 order-db,连接池上限 100"
"DBA 是张三,紧急联系方式是 xxx"
→ 记录"系统的知识"和"组织的知识"
长期记忆的检索方式 :当 Agent 需要回忆时,它不可能把所有历史记忆都塞进上下文窗口。需要一种机制来检索最相关的记忆片段------这正是 RAG(检索增强生成)发挥作用的地方。
2.3 工作台记忆:Scratchpad
工作台记忆(Scratchpad)是一种特殊的中间状态记录,类似于程序员调试时的断点监视器[4]。
latex
Scratchpad 示例:
任务:"诊断订单服务报错并修复"
当前状态:
已完成步骤:
Step 1: 查询日志 → 发现数据库连接超时 ✅
Step 2: 查询监控 → 确认错误率 15% ✅
当前步骤:
Step 3: 查询数据库锁状态 ⏳ 进行中
中间变量:
错误类型 = "数据库连接超时"
可能根因 = ["锁争用", "连接池耗尽", "数据库宕机"]
已排除 = ["数据库宕机"(监控显示 DB 存活)]
下一步计划:
如果锁争用 → kill 阻塞 SQL
如果连接池耗尽 → 扩容连接池
Scratchpad 与工作记忆的区别:工作记忆是 LLM 的原始上下文(包含所有消息),Scratchpad 是结构化的执行状态摘要。它让 Agent 在任意时刻都知道"我做到了哪一步、中间结论是什么、接下来该做什么"。
在工程实现中,Scratchpad 通常以 JSON 或结构化文本的形式注入到系统 Prompt 中:
python
scratchpad = {
"task": "诊断订单服务报错并修复",
"completed_steps": [
{"step": 1, "action": "查询日志", "result": "数据库连接超时"},
{"step": 2, "action": "查询监控", "result": "错误率 15%"}
],
"current_step": {"step": 3, "action": "查询数据库锁状态", "status": "进行中"},
"variables": {
"error_type": "数据库连接超时",
"possible_causes": ["锁争用", "连接池耗尽", "数据库宕机"],
"eliminated": ["数据库宕机"]
}
}
2.4 三种记忆的对比
| 维度 | 工作记忆 | 长期记忆 | 工作台记忆 |
|---|---|---|---|
| 对应实现 | LLM 上下文窗口 | 向量数据库 / 外部存储 | 结构化状态记录 |
| 类比 | 草稿纸 | 笔记本 | 任务看板 |
| 生命周期 | 单次任务 | 跨任务持久 | 单次任务 |
| 容量 | 受 Token 限制 | 理论无限 | 不占上下文空间(可选注入) |
| 检索方式 | 全量注入(LLM 直接看到) | 语义检索(按相关性选取) | 按需注入 |
| 核心挑战 | 容量有限,需要压缩 | 检索准确率 | 结构化维护成本 |
| 缺少时的症状 | 健忘(重复操作) | 不会借鉴历史经验 | 偏离目标(不知道做到哪了) |
3. RAG:Agent 长期记忆的"检索引擎"
在讨论长期记忆时,一个绕不开的技术是 RAG(Retrieval-Augmented Generation,检索增强生成) [5]。在展开之前,需要先澄清一个读者最常混淆的问题。
3.1 RAG 和 Agent 是什么关系?
latex
┌─────────────────────────────────────────────────────────────┐
│ │
│ RAG 和 Agent 的关系 │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ │ │ │ │
│ │ RAG │ │ Agent │ │
│ │ │ │ │ │
│ │ 一种技术手段 │ │ 一种系统形态 │ │
│ │ "怎么找到正确的 │ │ "怎么自主完成 │ │
│ │ 知识来回答" │ │ 一个复杂任务" │ │
│ │ │ │ │ │
│ │ 核心动作: │ │ 核心动作: │ │
│ │ 检索 → 注入 → 生成 │ │ 感知 → 推理 → 行动 │ │
│ │ │ │ │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ └────→│ 交集: │←─────┘ │
│ │ Agent 用 RAG │ │
│ │ 增强长期记忆 │ │
│ └─────────────┘ │
│ │
│ 关键结论: │
│ • RAG 可以独立存在(没有 Agent,纯问答系统也用 RAG) │
│ • Agent 可以不使用 RAG(只用上下文窗口管理记忆) │
│ • 两者结合时,RAG 是 Agent 记忆层的一种实现方式 │
│ │
└─────────────────────────────────────────────────────────────┘
一句话总结:RAG 是一种技术手段,Agent 是一种系统形态。Agent 可以用 RAG 来增强记忆,但 Agent 不等于 RAG,RAG 也不等于 Agent。
不需要先学 RAG 再学 Agent。本篇会讲清楚 Agent 如何使用 RAG 的结果来增强记忆能力。RAG 的完整工程化(文档分块、Embedding 模型选择、检索策略优化、幻觉控制等)将在 RAG 工程化指南系列中详细展开。
3.2 RAG 在 Agent 中怎么工作
RAG 作为 Agent 长期记忆的检索引擎,工作流程如下[5]:
latex
Step 1: 存储(离线阶段)
Agent 每完成一个任务,将关键信息存入长期记忆:
┌────────────────────────────────────────────────────┐
│ 存储内容: │
│ "2026-03-15 订单服务数据库连接超时。 │
│ 根因:锁争用(一条未加索引的全表查询持有行锁 45 分钟)│
│ 解决方案:kill 阻塞 SQL [ID: 8837]。 │
│ 恢复时间:kill 后 30 秒错误率恢复正常。" │
└────────────────────────────────────────────────────┘
↓
存入向量数据库(文本被转为向量,支持语义检索)
Step 2: 检索(在线阶段)
新任务到来:"订单服务报错了,帮我看看"
Agent 在调用工具之前,先检索长期记忆:
┌────────────────────────────────────────────────────┐
│ 检索查询:"订单服务报错" │
│ ↓ │
│ 语义检索 → 找到最相关的历史记录: │
│ 1. "2026-03-15 订单服务数据库连接超时..."(相似度 0.92)│
│ 2. "2026-02-20 用户服务 OOM..."(相似度 0.45) │
│ 3. "2026-01-10 支付服务第三方超时..."(相似度 0.38)│
└────────────────────────────────────────────────────┘
↓
将最相关的结果注入到 Agent 的上下文窗口中
Step 3: 增强推理
Agent 的上下文窗口中现在多了历史信息:
┌────────────────────────────────────────────────────┐
│ System Prompt: "你是一个 DevOps 工程师..." │
│ │
│ [长期记忆检索结果] │
│ "历史参考:2026-03-15 订单服务曾出现类似问题, │
│ 根因是数据库锁争用,解决方案是 kill 阻塞 SQL。 │
│ 你可以优先排查这个方向。" │
│ │
│ User: "订单服务报错了,帮我看看" │
└────────────────────────────────────────────────────┘
↓
Agent 推理时有了历史参考:
Thought: "上周类似问题是锁争用引起的,我先排查这个方向。"
→ 调用数据库诊断 Skill → 确认是锁争用 → 快速解决
这就是 RAG 增强 Agent 记忆的完整流程。Agent 不是每次都从零开始排查,而是先"回忆"历史经验,用历史经验指导当前推理。
3.3 RAG 涉及哪些技术?(指向 RAG 系列)
RAG 在工程落地中涉及大量技术细节,这些将在 RAG 工程化指南系列中详细展开:
latex
RAG 工程化的技术栈(将在 RAG 系列中覆盖):
文档处理层:
文档解析(PDF/HTML/代码)→ 分块策略(固定/语义/递归)→ 清洗
向量化层:
Embedding 模型选择 → 向量维度 → 批量编码
存储层:
向量数据库选型(Milvus/Pinecone/Weaviate/Chroma)
检索层:
向量检索 → 关键词检索(BM25)→ 混合检索 → 重排序(Reranking)
生成层:
上下文注入策略 → 事实校验 → 引用溯源 → 幻觉控制
评测层:
检索准确率(Recall@K / MRR)→ 端到端质量 → 持续改进
本篇只关注 Agent 如何使用 RAG 的结果,不深入 RAG 的实现细节。
3.4 MCP 与 Agent 工具连接
在讨论 Agent 的记忆和工具连接时,另一个值得了解的概念是 MCP(Model Context Protocol) [6]。
MCP 和 Function Calling 是什么关系?
latex
Function Calling(函数调用):
LLM 决定"调用哪个函数、传什么参数"的机制
层面:LLM 的推理能力
角色:大脑说"我要调用 query_logs"
MCP(Model Context Protocol):
LLM 应用与外部工具/数据源之间的标准化连接协议
层面:系统的连接标准
角色:规定了工具怎么被发现、怎么被连接、怎么被调用
两者的关系:
Function Calling 是"怎么调"(LLM 侧的机制)
MCP 是"怎么连"(系统侧的协议)
两者互补,不冲突
latex
类比:
Function Calling = 你决定"我要打车"(大脑的决策)
MCP = 打车软件的标准接口(不管你用滴滴还是 Uber,
接口协议都是统一的:选目的地→叫车→上车→支付)
在 Agent 系统中,MCP 的价值在于标准化工具的注册和发现------Agent 不需要硬编码每个工具的调用方式,而是通过 MCP 协议自动发现可用的工具。关于 MCP 的详细规范和实现,将在 MCP 协议指南系列中展开。
4. Agent 的规划范式
记忆解决了"知不知道之前做过什么"的问题,规划解决的是"接下来该做什么"的问题[7][8]。
4.1 无规划:ReAct 的边走边看
上一篇介绍的 ReAct 模式本质上是一种"无全局规划"的推理方式------Agent 每一步都根据当前状态决定下一步做什么,不做全局计划。
latex
ReAct 模式下的排查过程:
Thought 1: "有告警,我先查日志"
→ 查日志 → 发现连接超时
Thought 2: "连接超时,我查查数据库"
→ 查数据库 → 发现锁争用
Thought 3: "有锁争用,我查查是哪条 SQL"
→ 查 SQL → 找到阻塞查询
Thought 4: "找到了,需要 kill"
→ kill → 恢复
这看起来没什么问题。但如果情况更复杂呢?
latex
复杂场景下的 ReAct:
Thought 1: "有告警,我先查日志"
→ 查日志 → 发现连接超时 + OOM + 上游超时
Thought 2: "有三种错误,我先看看 OOM"
→ 查内存 → 发现某个 Pod 内存很高
Thought 3: "内存高,我看看是不是有内存泄漏"
→ 查代码变更 → 最近有一次部署
Thought 4: "有部署,我先回滚试试"
→ 回滚 → 观察 → 没改善
Thought 5: "回滚没用,那不是代码问题。我再看看数据库"
→ 查数据库 → 发现锁争用
Thought 6: "锁争用是根因。但等等,我之前回滚了一次,
需要先恢复回滚..."
→ 重新部署 → kill 阻塞 SQL → 恢复
没有规划的 Agent 浪费了时间在"回滚代码"这个错误方向上,最终绕了一圈才找到真正的根因。如果有全局规划,Agent 可以先分析所有错误的关联性,判断"OOM 和上游超时很可能是数据库连接超时引起的连锁反应",直接从数据库方向入手。
4.2 先规划后执行
Plan-then-Execute 模式的核心是:在执行任何操作之前,先让 LLM 生成一个结构化的执行计划 [7][8]。
latex
Plan-then-Execute 模式下的排查过程:
═══════════════════════════════════════
阶段一:分析 + 规划
═══════════════════════════════════════
LLM 收到告警信息,生成计划:
{
"analysis": "告警包含三种错误(连接超时、OOM、上游超时),
很可能是同一个根因引起的连锁反应。
优先排查数据库(连接超时是直接原因)。",
"steps": [
{"id": 1, "action": "查询数据库锁状态", "skill": "db_diagnosis",
"reason": "连接超时的最常见原因是锁争用"},
{"id": 2, "action": "根据锁状态决定", "skill": "conditional",
"condition": "如果有锁争用 → kill 阻塞 SQL;如果无 → 扩容连接池"},
{"id": 3, "action": "验证服务恢复", "skill": "query_metrics"},
{"id": 4, "action": "生成复盘报告", "skill": "report"}
]
}
═══════════════════════════════════════
阶段二:逐步执行
═══════════════════════════════════════
Execute Step 1: 查询数据库锁状态
→ 发现锁争用
Execute Step 2: 有锁争用 → kill 阻塞 SQL [需人工确认]
→ 用户确认 → 执行
Execute Step 3: 查询监控 → 错误率恢复正常
Execute Step 4: 生成复盘报告
Plan-then-Execute 的优势是全局视角------Agent 在第一步就考虑了所有错误的关联性,直接选对了排查方向。
4.3 动态重规划
Plan-then-Execute 的风险是:计划可能过时。如果执行过程中环境变了怎么办?
latex
场景:计划执行到一半,发现新情况
原计划:
Step 1: 查询数据库锁状态 ✅
Step 2: kill 阻塞 SQL ⏳
执行 Step 1 时发现:
→ 不仅锁争用,数据库磁盘空间也只剩 2%
→ 原计划没有包含处理磁盘空间的步骤
需要触发重规划(Re-planning):
→ 将新发现反馈给 LLM
→ LLM 生成更新后的计划:
{
"updated_plan": [
{"id": 1, "action": "查询数据库锁状态", "status": "已完成"},
{"id": 2, "action": "清理磁盘空间", "skill": "disk_cleanup", ← 新增
"reason": "磁盘空间仅剩 2%,需要先释放空间"},
{"id": 3, "action": "kill 阻塞 SQL", "skill": "db_management"},
{"id": 4, "action": "验证服务恢复", "skill": "query_metrics"},
{"id": 5, "action": "生成复盘报告", "skill": "report"}
]
}
重规划的触发条件 通常包括[8]:
latex
触发重规划的条件:
1. 工具执行返回了计划中未预料的错误
2. 工具执行的结果与预期不符
3. 发现了计划中未考虑的新信息(如磁盘空间不足)
4. 某个步骤执行超时或失败后重试仍然失败
避免过度重规划的条件:
1. 最大重规划次数(如最多重规划 3 次)
2. 重规划触发的时间间隔(如两次重规划至少间隔 30 秒)
3. 重规划的判断必须基于明确的异常信号,不能"感觉不太对就重规划"
4.4 三种规划范式的对比
| 维度 | 无规划(ReAct) | 先规划后执行 | 动态重规划 |
|---|---|---|---|
| 核心思路 | 边走边看 | 先画地图再走 | 走的过程中修正地图 |
| 全局视野 | 弱 | 强 | 强(通过重规划维持) |
| 灵活性 | 高 | 低(计划固定) | 中(重规划有开销) |
| Token 成本 | 中(每步推理) | 较低(规划一次) | 中高(规划 + 可能的重规划) |
| 适合场景 | 简单任务、步骤少 | 流程明确、步骤可预知 | 复杂任务、环境不确定 |
| DevOps 示例 | 简单的单服务排查 | 发布保障流程 | 复杂的分布式故障排查 |
| 计划数据结构 | 无 | 列表 / DAG | 列表 / DAG + 版本控制 |
5. 反思与自我纠错
当工具执行返回错误时,Agent 应该如何处理?
5.1 三种错误处理策略
策略一:盲目重试(最差)
latex
Agent 调用修复工具 → 返回:"权限不足"
Agent 再次调用修复工具 → 返回:"权限不足"
Agent 再次调用修复工具 → 返回:"权限不足"
→ 浪费 Token,毫无进展
策略二:错误分析(推荐)
latex
Agent 调用修复工具 → 返回:"权限不足"
Agent Thought: "权限不足说明我没有数据库管理的权限。
这个操作需要 DBA 执行。
我应该将问题升级给 DBA,而不是继续重试。"
→ 切换策略:生成工单 → 转给 DBA
策略三:策略调整(最优)
latex
Agent 调用修复工具 → 返回:"权限不足"
Agent Thought: "我没有 kill SQL 的权限。
但我可以尝试其他方式:
方案 A:转给 DBA(需要等待)
方案 B:调整数据库配置参数,限制锁持有时间(我有配置权限)
方案 B 能更快缓解问题,先试 B。"
→ 调用配置修改工具 → 设置 lock_timeout=30s → 观察 → 阻塞 SQL 自动超时释放
5.2 Reflexion 框架的工程应用
上一篇介绍了 Reflexion 的概念。在工程实现中,Reflexion 可以用一个简单的反思 Prompt 来实现[9]:
latex
当 Agent 判断当前任务失败时,调用反思:
反思 Prompt:
"你在执行任务 [任务描述] 时失败了。
你尝试了 [已执行的步骤列表]。
最后的错误是 [错误信息]。
请反思:
1. 失败的根本原因是什么?
2. 你的哪个假设是错误的?
3. 下次遇到类似情况,应该怎么调整策略?
以 JSON 格式输出反思结论。"
反思结果存入长期记忆 → 下次遇到类似问题时自动检索
6. 上下文工程的实战技巧
所有记忆机制最终都要组装到 LLM 的上下文窗口中。上下文工程(Context Engineering) 是指如何在有限的 Token 预算内,为 LLM 提供最有效的信息[2]。
6.1 上下文组装配方
一个精心设计的上下文通常包含以下部分,按优先级从高到低排列:
latex
┌─────────────────────────────────────────────────────────────┐
│ 上下文窗口组装配方 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ① System Prompt(系统指令) ← 必须保留 │ │
│ │ 角色定义、行为约束、安全规则 │ │
│ │ 约 500-2000 tokens │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ ② Scratchpad(工作台状态) ← 强烈建议保留 │ │
│ │ 当前任务进度、已完成步骤、中间变量 │ │
│ │ 约 200-1000 tokens │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ ③ 长期记忆检索结果 ← 按相关性注入 │ │
│ │ 与当前任务最相关的历史经验 │ │
│ │ 约 500-2000 tokens(Top 3 结果) │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ ④ 可用 Skill 列表 ← 必须保留 │ │
│ │ Function Calling 的函数描述 │ │
│ │ 约 200-500 tokens / Skill │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ ⑤ 最近的对话历史 ← 优先保留 │ │
│ │ 最近 N 轮的 Thought + Action + Observation │ │
│ │ 约 300-800 tokens / 轮 │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ ⑥ 早期对话历史的摘要 ← 可压缩 │ │
│ │ 用 LLM 生成的摘要替代原始消息 │ │
│ │ 约 200-500 tokens │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 总 Token 预算示例(以 8K 窗口为例): │
│ ① System Prompt 1,500 │
│ ② Scratchpad 500 │
│ ③ 长期记忆检索 1,500 │
│ ④ Skill 列表 1,000 (2-3 个 Skill) │
│ ⑤ 最近对话 2,500 (3-5 轮) │
│ ⑥ 早期摘要 500 │
│ ────────────────── │
│ 合计 7,500 / 8,000 │
│ │
└─────────────────────────────────────────────────────────────┘
6.2 Token 预算不足时的取舍策略
当上下文窗口不够大时(比如早期模型只有 4K 或 8K 窗口),需要做取舍[2]:
latex
取舍优先级(从最重要到最不重要):
① System Prompt → 绝对不能删(决定了 Agent 的行为模式)
② 最近的对话历史 → 最后 2-3 轮必须保留(维持对话连贯性)
③ Scratchpad → 保留(否则 Agent 不知道自己做到哪了)
④ Skill 列表 → 只保留当前最可能用到的 Skill 描述
⑤ 长期记忆检索 → 只保留最相关的 1-2 条
⑥ 早期对话历史 → 压缩为摘要或直接丢弃
7. 动手练习:给最小 Agent 添加记忆和规划能力
现在,我们在第一篇的最小 Agent 基础上,逐步添加三种能力:工作记忆管理、长期记忆检索、任务规划。
前置条件:已完成第一篇的最小 Agent 代码,并能正常运行。以下代码基于第一篇的纯 Python 版本进行增量修改。
7.1 Step 1:添加工作记忆管理(上下文压缩)
解决的问题:Agent 执行多步后消息太多,超出 Token 限制或浪费成本。
实现策略:当消息数量超过阈值时,用 LLM 将早期消息压缩为摘要。
python
"""
Agent v2 - 添加工作记忆管理
在第一篇最小 Agent 基础上,增加上下文压缩能力。
当消息数量超过阈值时,自动将早期消息压缩为摘要。
新增依赖:无(仍使用 openai + python-dotenv)
"""
"""
Agent v2 - 添加工作记忆管理 (修复压缩Bug + 调低阈值确保触发)
"""
import os
import json
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
if not api_key or api_key == "sk-your-api-key-here":
print("错误:请在 .env 文件中设置有效的 OPENAI_API_KEY")
exit(1)
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
client = OpenAI(**client_kwargs)
print(f"当前使用的模型: {model_name} | API地址: {base_url or '官方默认'}\n")
# ── 定义工具 ──
tools = [
{"type": "function", "function": {"name": "query_logs", "description": "查询指定服务的日志", "parameters": {"type": "object", "properties": {"service": {"type": "string"}, "time_range": {"type": "string"}}, "required": ["service", "time_range"]}}},
{"type": "function", "function": {"name": "query_metrics", "description": "查询指定服务的监控指标", "parameters": {"type": "object", "properties": {"service": {"type": "string"}}, "required": ["service"]}}},
{"type": "function", "function": {"name": "kill_query", "description": "终止指定的数据库查询", "parameters": {"type": "object", "properties": {"query_id": {"type": "string"}}, "required": ["query_id"]}}},
{"type": "function", "function": {"name": "verify_recovery", "description": "验证服务是否恢复正常", "parameters": {"type": "object", "properties": {"service": {"type": "string"}}, "required": ["service"]}}}
]
def execute_tool(name, args):
if name == "query_logs": return f"[日志] {args['service']} 有大量数据库连接超时错误"
elif name == "query_metrics": return f"[监控] {args['service']} 错误率 15%"
elif name == "kill_query": return f"[执行] 已终止查询 {args['query_id']},锁已释放"
elif name == "verify_recovery": return f"[验证] {args['service']} 错误率 0.1%,已恢复"
return "未知工具"
# ── 上下文压缩功能 ──
# 调低阈值以触发演示,按需设置
MAX_MESSAGES_BEFORE_COMPRESS = 8
KEEP_RECENT_MESSAGES = 2
def get_msg_text(m):
if m.get("content"): return m["content"]
if m.get("tool_calls"):
texts = []
for tc in m["tool_calls"]:
name = tc['function']['name'] if isinstance(tc, dict) else tc.function.name
args = tc['function']['arguments'] if isinstance(tc, dict) else tc.function.arguments
texts.append(f"调用工具 {name}({args})")
return "; ".join(texts)
return ""
def compress_messages(messages):
if len(messages) <= MAX_MESSAGES_BEFORE_COMPRESS:
return messages
system_prompt = messages[0]
# 修复1:向前回溯,确保不在 tool 消息中间切断
keep_count = KEEP_RECENT_MESSAGES
while keep_count < len(messages) and messages[-keep_count].get("role") == "tool":
keep_count += 1
recent_messages = messages[-keep_count:]
old_messages = messages[1:-keep_count] if keep_count < len(messages) else []
if not old_messages: return messages
# 修复2:提取工具调用细节给摘要模型
old_msgs_text = [{"role": m.get("role"), "content": get_msg_text(m)} for m in old_messages]
summary_prompt = [
{"role": "system", "content": "将以下对话历史压缩为简洁摘要,保留关键信息。"},
{"role": "user", "content": json.dumps(old_msgs_text, ensure_ascii=False)}
]
try:
summary_response = client.chat.completions.create(model=model_name, messages=summary_prompt)
summary = summary_response.choices[0].message.content
print(f"\n📝 上下文压缩触发:{len(old_messages)} 条早期消息 → 1 条摘要\n")
except Exception as e:
print(f"⚠️ 摘要生成失败: {e},回退为简单截断")
summary = "早期对话已省略。"
return [system_prompt, {"role": "system", "content": f"[历史摘要] {summary}"}] + recent_messages
# ── Agent 核心循环 ──
def run_agent(user_input):
messages = [
{"role": "system", "content": "你是一个严谨的 DevOps 工程师。请一步步排查,使用 verify_recovery 验证修复结果后再回复用户。"},
{"role": "user", "content": user_input}
]
for i in range(10):
print(f"--- 循环第 {i+1} 轮 (当前上下文消息数: {len(messages)}) ---")
messages = compress_messages(messages)
response = client.chat.completions.create(model=model_name, messages=messages, tools=tools)
msg = response.choices[0].message
if msg.tool_calls:
messages.append(msg.model_dump())
for tool_call in msg.tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f"🔵 调用工具: {name},参数: {args}")
result = execute_tool(name, args)
print(f"🟢 工具返回: {result}")
messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
else:
print("🤖 Agent 生成最终回复")
return msg.content
if __name__ == "__main__":
print("=" * 50)
print("Agent v2 --- 带工作记忆管理")
print("=" * 50)
result = run_agent("订单服务报错了,请帮我排查根因,执行修复操作,并确认服务恢复正常")
print("\n====================\n最终输出:\n", result)
latex
运行效果(关注上下文压缩的触发):
==================================================
Agent v2 --- 带工作记忆管理
==================================================
当前使用的模型: gpt-4o | API地址: 官方默认
--- 循环第 1 轮 (上下文消息数: 2) ---
🔵 调用工具: query_logs,参数: {'service': 'order-service', 'time_range': 'last_30min'}
🟢 工具返回: [日志] order-service 在 last_30min 内有大量数据库连接超时错误
--- 循环第 2 轮 (上下文消息数: 5) ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 3 轮 (上下文消息数: 8) ---
🔵 调用工具: kill_query,参数: {'query_id': '8837'}
🟢 工具返回: [执行] 已终止查询 8837,锁已释放
--- 循环第 4 轮 (上下文消息数: 11) ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 5 轮 (上下文消息数: 14) ---
📝 上下文压缩:9 条消息 → 摘要
🤖 Agent 生成最终回复
====================
最终输出:
根据诊断,订单服务的数据库连接超时是由锁争用引起的...
7.2 Step 2:添加长期记忆(简易 RAG)
解决的问题:Agent 每次都从零开始,不知道历史经验。
实现策略:用一个简单的内存字典存储历史诊断结论,通过关键词匹配检索相关记忆。
说明:这里使用简单的关键词匹配来降低依赖门槛。生产环境中应使用 Embedding 模型 + 向量数据库实现语义检索,详见 RAG 工程化指南系列。
python
"""
Agent v3 - 添加长期记忆
在 v2 基础上,增加简单的长期记忆能力。
Agent 每完成一个任务,将关键信息存入记忆;
遇到新问题时,先检索历史记忆。
"""
import os
import json
import time
from datetime import datetime
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
if not api_key or api_key == "sk-your-api-key-here":
print("错误:请在 .env 文件中设置有效的 OPENAI_API_KEY")
exit(1)
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
client = OpenAI(**client_kwargs)
print(f"当前使用的模型: {model_name} | API地址: {base_url or '官方默认'}\n")
# ── 工具定义(同 v2)──
tools = [
{
"type": "function",
"function": {
"name": "query_logs",
"description": "查询指定服务在指定时间范围内的日志",
"parameters": {
"type": "object",
"properties": {
"service": {"type": "string", "description": "服务名称"},
"time_range": {"type": "string", "description": "时间范围,如 last_30min"}
},
"required": ["service", "time_range"]
}
}
},
{
"type": "function",
"function": {
"name": "query_metrics",
"description": "查询指定服务的监控指标(错误率、延迟等)",
"parameters": {
"type": "object",
"properties": {
"service": {"type": "string", "description": "服务名称"}
},
"required": ["service"]
}
}
},
{
"type": "function",
"function": {
"name": "kill_query",
"description": "终止指定的数据库查询(需人工确认)",
"parameters": {
"type": "object",
"properties": {
"query_id": {"type": "string", "description": "要终止的查询 ID"}
},
"required": ["query_id"]
}
}
}
]
# ── 带状态的模拟工具执行 ──
# 模拟状态:记录 Agent 已执行的修复操作
_sim_state = {"fixed": False}
def execute_tool(name, args):
"""模拟工具执行。实际项目中这里会调用真实的 API。"""
if name == "query_logs":
if _sim_state["fixed"]:
return f"[日志] {args['service']} 在 {args['time_range']} 内日志正常,无异常错误"
return f"[日志] {args['service']} 在 {args['time_range']} 内有大量数据库连接超时错误"
elif name == "query_metrics":
if _sim_state["fixed"]:
return f"[监控] {args['service']} 当前错误率 0.1%,P99 延迟 120ms,已恢复正常"
return f"[监控] {args['service']} 当前错误率 15%,P99 延迟 8.2s"
elif name == "kill_query":
_sim_state["fixed"] = True # 执行修复后,标记为已修复
return f"[执行] 已终止查询 {args['query_id']},锁已释放"
return "未知工具"
# ── 新增:长期记忆存储 ──
class LongTermMemory:
"""
简易长期记忆存储。
生产环境中应替换为向量数据库(如 Chroma / Milvus / Pinecone)。
"""
def __init__(self):
self.memories = []
def store(self, content, tags=None):
"""存储一条记忆"""
memory = {
"content": content,
"tags": tags or [],
"timestamp": datetime.now().isoformat()
}
self.memories.append(memory)
print(f"💾 长期记忆:已存储 → {content[:80]}...")
def search(self, query, top_k=3):
"""
检索与 query 相关的记忆。
简易实现:基于关键词匹配。
生产环境中应使用 Embedding 向量相似度检索。
"""
if not self.memories:
return []
query_lower = query.lower()
scored = []
for mem in self.memories:
# 简单的关键词匹配评分
score = sum(
1 for word in query_lower.split()
if word in mem["content"].lower() or
any(word in tag.lower() for tag in mem["tags"])
)
if score > 0:
scored.append((score, mem))
scored.sort(key=lambda x: x[0], reverse=True)
results = [mem for _, mem in scored[:top_k]]
if results:
print(f"🧠 长期记忆检索:找到 {len(results)} 条相关记忆")
return results
# 初始化长期记忆
memory = LongTermMemory()
# 预置一些历史记忆(模拟之前任务的经验)
memory.store(
"2026-03-15 订单服务数据库连接超时,根因是锁争用,"
"一条未加索引的全表查询持有行锁 45 分钟。"
"解决方案:kill 阻塞 SQL [ID: 8837],30 秒后服务恢复。",
tags=["order-service", "database", "lock", "timeout"]
)
memory.store(
"2026-02-20 用户服务 OOM,根因是内存泄漏(新版本的缓存未清理)。"
"解决方案:回滚到上一版本。",
tags=["user-service", "oom", "memory"]
)
# 新增:上下文压缩功能
# ── 上下文压缩功能(健壮版)(同 v2)──
MAX_MESSAGES_BEFORE_COMPRESS = 12
KEEP_RECENT_MESSAGES = 4
def get_msg_text(m):
"""安全提取消息文本,处理工具调用的情况"""
if m.get("content"):
return m["content"]
if m.get("tool_calls"):
# 兼容 OpenAI SDK 对象和 dict
texts = []
for tc in m["tool_calls"]:
name = tc.function.name if hasattr(tc, 'function') else tc['function']['name']
args = tc.function.arguments if hasattr(tc, 'function') else tc['function']['arguments']
texts.append(f"调用工具 {name}({args})")
return "; ".join(texts)
return ""
def compress_messages(messages):
"""
当消息过多时,将早期消息压缩为摘要。
修复:1. 保证不切断 tool_calls 配对;
2. 保留工具调用细节传给摘要模型。
"""
if len(messages) <= MAX_MESSAGES_BEFORE_COMPRESS:
return messages
system_prompt = messages[0]
# 修复 1:向前回溯,确保不在 tool 消息中间切断
keep_count = KEEP_RECENT_MESSAGES
while keep_count < len(messages) and messages[-keep_count].get("role") == "tool":
keep_count += 1 # 找到配对的 assistant 消息一起保留
recent_messages = messages[-keep_count:]
old_messages = messages[1:-keep_count] if keep_count < len(messages) else []
if not old_messages:
return messages
# 修复 2:提取工具调用细节,而不是简单替换为 [工具调用]
old_msgs_text = [
{"role": m.get("role"), "content": get_msg_text(m)}
for m in old_messages
]
summary_prompt = [
{"role": "system", "content": (
"你是一个摘要助手。将以下对话历史压缩为简洁的摘要,"
"保留关键信息(诊断结论、执行了什么工具调用、"
"工具返回结果、已排除的假设)。"
)},
{"role": "user", "content": json.dumps(old_msgs_text, ensure_ascii=False)}
]
try:
summary_response = client.chat.completions.create(
model=model_name,
messages=summary_prompt
)
summary = summary_response.choices[0].message.content
print(f"📝 上下文压缩:{len(old_messages)} 条消息 → 摘要")
except Exception as e:
print(f"⚠️ 摘要生成失败: {e},回退为简单截断")
summary = "早期对话已省略。"
return [
system_prompt,
{"role": "system", "content": f"[历史摘要] {summary}"}
] + recent_messages
# ── Agent 核心循环(带长期记忆检索)──
def run_agent(user_input):
_sim_state["fixed"] = False # 重置模拟状态
# ① 新增:在推理之前,先检索长期记忆
relevant_memories = memory.search(user_input)
# ② 组装 System Prompt(注入记忆)
system_content = (
"你是一个 DevOps 工程师。分析问题并调用合适的工具来诊断和解决问题。"
"在调用高风险操作前,先说明你的判断和方案。"
)
if relevant_memories:
memory_text = "\n".join([f"- {m['content']}" for m in relevant_memories])
system_content += f"\n\n[历史经验参考]\n{memory_text}"
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": user_input}
]
max_iterations = 10
for i in range(max_iterations):
print(f"\n--- 循环第 {i+1} 轮 (上下文消息数: {len(messages)}) ---")
messages = compress_messages(messages)
response = client.chat.completions.create(
model=model_name, messages=messages, tools=tools
)
msg = response.choices[0].message
if msg.tool_calls:
messages.append(msg)
for tool_call in msg.tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f"🔵 调用工具: {name},参数: {args}")
result = execute_tool(name, args)
print(f"🟢 工具返回: {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
print("🤖 Agent 生成最终回复")
# ③ 新增:任务完成后,将诊断结论存入长期记忆
memory.store(
f"{datetime.now().strftime('%Y-%m-%d')} 任务:{user_input}。"
f"结论:{msg.content[:200]}",
tags=[t.lower() for t in user_input.split() if len(t) > 2]
)
return msg.content
return "达到最大迭代次数,任务未完成。"
if __name__ == "__main__":
print("=" * 50)
print("Agent v3 --- 带长期记忆")
print("=" * 50)
# 第一轮任务
print("\n【任务 1】")
result1 = run_agent("订单服务报错了,帮我看看什么原因")
print(f"\n结果:{result1[:100]}...")
# 第二轮:验证 Agent 是否记住了历史经验
print("\n" + "=" * 50)
print("此时长期记忆中有以下记录:")
for i, m in enumerate(memory.memories):
print(f" [{i+1}] {m['content'][:80]}...")
print("=" * 50)
print("\n【任务 2 --- 新的类似问题】")
result2 = run_agent("订单服务又出问题了,错误率飙升")
print(f"\n结果:{result2[:100]}...")
latex
运行效果(关注长期记忆的检索和注入):
==================================================
Agent v3 --- 带长期记忆
==================================================
当前使用的模型: gpt-4o | API地址: 官方默认
【任务 1】
🧠 长期记忆检索:找到 1 条相关记忆
--- 循环第 1 轮 ---
🔵 调用工具: query_logs,参数: {'service': 'order-service', 'time_range': 'last_30min'}
🟢 工具返回: [日志] order-service 在 last_30min 内有大量数据库连接超时错误
--- 循环第 2 轮 ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 3 轮 ---
🔵 调用工具: kill_query,参数: {'query_id': '8837'}
🟢 工具返回: [执行] 已终止查询 8837,锁已释放
--- 循环第 4 轮 ---
🤖 Agent 生成最终回复
💾 长期记忆:已存储 → 2026-03-20 任务:订单服务报错了...
==================================================
此时长期记忆中有以下记录:
[1] 2026-03-15 订单服务数据库连接超时,根因是锁争用...
[2] 2026-02-20 用户服务 OOM,根因是内存泄漏...
[3] 2026-03-20 任务:订单服务报错了...结论:根因是锁争用...
==================================================
【任务 2 --- 新的类似问题】
🧠 长期记忆检索:找到 2 条相关记忆
--- 循环第 1 轮 ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 2 轮 ---
🔵 调用工具: query_logs,参数: {'service': 'order-service', 'time_range': 'last_30min'}
🟢 工具返回: [日志] order-service 在 last_30min 内有大量数据库连接超时错误
--- 循环第 3 轮 ---
🤖 Agent 生成最终回复
💾 长期记忆:已存储 → 2026-03-20 任务:订单服务又出问题了...
注意:第二次任务中,Agent 检索到了历史经验("上次类似问题是锁争用"),
推理时直接从数据库方向入手,排查效率更高。
7.3 Step 3:添加任务规划
解决的问题:Agent "瞎想"------没有全局计划,在排查中迷路。
实现策略:在执行任何工具调用之前,先让 LLM 生成一个结构化的执行计划。
python
"""
Agent v4 - 添加任务规划
在 v3 基础上,增加任务规划能力。
Agent 在调用工具之前,先生成一个结构化的执行计划。
"""
import os
import json
from datetime import datetime
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
if not api_key or api_key == "sk-your-api-key-here":
print("错误:请在 .env 文件中设置有效的 OPENAI_API_KEY")
exit(1)
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
client = OpenAI(**client_kwargs)
print(f"当前使用的模型: {model_name} | API地址: {base_url or '官方默认'}\n")
# ── 工具定义(同 v3)──
tools = [
{
"type": "function",
"function": {
"name": "query_logs",
"description": "查询指定服务在指定时间范围内的日志",
"parameters": {
"type": "object",
"properties": {
"service": {"type": "string"},
"time_range": {"type": "string"}
},
"required": ["service", "time_range"]
}
}
},
{
"type": "function",
"function": {
"name": "query_metrics",
"description": "查询指定服务的监控指标(错误率、延迟等)",
"parameters": {
"type": "object",
"properties": {
"service": {"type": "string"}
},
"required": ["service"]
}
}
},
{
"type": "function",
"function": {
"name": "kill_query",
"description": "终止指定的数据库查询(需人工确认)",
"parameters": {
"type": "object",
"properties": {
"query_id": {"type": "string"}
},
"required": ["query_id"]
}
}
}
]
# ── 带状态的模拟工具执行 ──
# 模拟状态:记录 Agent 已执行的修复操作
_sim_state = {"fixed": False}
def execute_tool(name, args):
"""模拟工具执行。实际项目中这里会调用真实的 API。"""
if name == "query_logs":
if _sim_state["fixed"]:
return f"[日志] {args['service']} 在 {args['time_range']} 内日志正常,无异常错误"
return f"[日志] {args['service']} 在 {args['time_range']} 内有大量数据库连接超时错误"
elif name == "query_metrics":
if _sim_state["fixed"]:
return f"[监控] {args['service']} 当前错误率 0.1%,P99 延迟 120ms,已恢复正常"
return f"[监控] {args['service']} 当前错误率 15%,P99 延迟 8.2s"
elif name == "kill_query":
_sim_state["fixed"] = True # 执行修复后,标记为已修复
return f"[执行] 已终止查询 {args['query_id']},锁已释放"
return "未知工具"
# ── 长期记忆(同 v3)──
class LongTermMemory:
def __init__(self):
self.memories = []
def store(self, content, tags=None):
self.memories.append({
"content": content, "tags": tags or [],
"timestamp": datetime.now().isoformat()
})
print(f"💾 长期记忆:已存储 → {content[:80]}...")
def search(self, query, top_k=3):
if not self.memories:
return []
query_lower = query.lower()
scored = []
for mem in self.memories:
score = sum(1 for w in query_lower.split()
if w in mem["content"].lower() or
any(w in t.lower() for t in mem["tags"]))
if score > 0:
scored.append((score, mem))
scored.sort(key=lambda x: x[0], reverse=True)
results = [m for _, m in scored[:top_k]]
if results:
print(f"🧠 长期记忆检索:找到 {len(results)} 条相关记忆")
return results
memory = LongTermMemory()
memory.store(
"2026-03-15 订单服务数据库连接超时,根因是锁争用。"
"解决方案:kill 阻塞 SQL [ID: 8837],30 秒后恢复。",
tags=["order-service", "database", "lock", "timeout"]
)
# ── 新增:任务规划功能 ──
def generate_plan(user_input, tools_desc, memory_context=""):
"""
在执行任何操作之前,让 LLM 生成一个结构化的执行计划。
增加了对不支持 JSON 模式的模型的降级处理。
"""
import re
plan_prompt = f"""你是一个 DevOps 工程师。请为以下任务生成一个执行计划。
任务:{user_input}
可用工具:
{tools_desc}
{f"历史经验参考:{memory_context}" if memory_context else ""}
请务必以 JSON 格式输出计划,不要包含任何其他文本:
{{
"analysis": "对问题的初步分析和判断",
"steps": [
{{"id": 1, "action": "做什么", "tool": "用哪个工具", "reason": "为什么这么做"}}
]
}}
要求:
- 计划不超过 5 步
- 每一步都要说明原因
- 优先排查最可能的根因方向
"""
try:
response = client.chat.completions.create(
model=model_name,
messages=[{"role": "user", "content": plan_prompt}],
response_format={"type": "json_object"}
)
plan = json.loads(response.choices[0].message.content)
except Exception:
# 降级策略:模型不支持 response_format 或解析失败
response = client.chat.completions.create(
model=model_name,
messages=[{"role": "user", "content": plan_prompt}]
)
raw = response.choices[0].message.content
match = re.search(r'\{.*\}', raw, re.DOTALL)
if match:
plan = json.loads(match.group(0))
else:
plan = {
"analysis": "无法生成结构化计划,转为直接排查",
"steps": [
{"id": 1, "action": "查询日志和监控",
"tool": "query_logs", "reason": "常规排查"}
]
}
return plan
# 新增:上下文压缩功能
# ── 上下文压缩功能(健壮版)(同 v2/v3)──
MAX_MESSAGES_BEFORE_COMPRESS = 12
KEEP_RECENT_MESSAGES = 4
def get_msg_text(m):
"""安全提取消息文本,处理工具调用的情况"""
if m.get("content"):
return m["content"]
if m.get("tool_calls"):
# 兼容 OpenAI SDK 对象和 dict
texts = []
for tc in m["tool_calls"]:
name = tc.function.name if hasattr(tc, 'function') else tc['function']['name']
args = tc.function.arguments if hasattr(tc, 'function') else tc['function']['arguments']
texts.append(f"调用工具 {name}({args})")
return "; ".join(texts)
return ""
def compress_messages(messages):
"""
当消息过多时,将早期消息压缩为摘要。
修复:1. 保证不切断 tool_calls 配对;
2. 保留工具调用细节传给摘要模型。
"""
if len(messages) <= MAX_MESSAGES_BEFORE_COMPRESS:
return messages
system_prompt = messages[0]
# 修复 1:向前回溯,确保不在 tool 消息中间切断
keep_count = KEEP_RECENT_MESSAGES
while keep_count < len(messages) and messages[-keep_count].get("role") == "tool":
keep_count += 1 # 找到配对的 assistant 消息一起保留
recent_messages = messages[-keep_count:]
old_messages = messages[1:-keep_count] if keep_count < len(messages) else []
if not old_messages:
return messages
# 修复 2:提取工具调用细节,而不是简单替换为 [工具调用]
old_msgs_text = [
{"role": m.get("role"), "content": get_msg_text(m)}
for m in old_messages
]
summary_prompt = [
{"role": "system", "content": (
"你是一个摘要助手。将以下对话历史压缩为简洁的摘要,"
"保留关键信息(诊断结论、执行了什么工具调用、"
"工具返回结果、已排除的假设)。"
)},
{"role": "user", "content": json.dumps(old_msgs_text, ensure_ascii=False)}
]
try:
summary_response = client.chat.completions.create(
model=model_name,
messages=summary_prompt
)
summary = summary_response.choices[0].message.content
print(f"📝 上下文压缩:{len(old_messages)} 条消息 → 摘要")
except Exception as e:
print(f"⚠️ 摘要生成失败: {e},回退为简单截断")
summary = "早期对话已省略。"
return [
system_prompt,
{"role": "system", "content": f"[历史摘要] {summary}"}
] + recent_messages
# ── Agent 核心循环(带规划 + 长期记忆 + 上下文压缩)──
def run_agent(user_input):
_sim_state["fixed"] = False # 重置模拟状态
# ① 检索长期记忆
relevant_memories = memory.search(user_input)
memory_text = "\n".join([f"- {m['content']}" for m in relevant_memories]) if relevant_memories else ""
# ② 生成执行计划
tools_desc = "\n".join([f"- {t['function']['name']}: {t['function']['description']}" for t in tools])
print("\n📋 正在生成执行计划...")
plan = generate_plan(user_input, tools_desc, memory_text)
print(f"\n📋 执行计划:")
print(f" 分析:{plan.get('analysis', '无')}")
for step in plan.get("steps", []):
print(f" Step {step['id']}: {step['action']} (工具: {step.get('tool', '无')})")
print(f" 原因: {step.get('reason', '无')}")
# ③ 按计划执行(但允许 LLM 在执行中动态调整)
system_content = (
"你是一个 DevOps 工程师。分析问题并调用合适的工具来诊断和解决问题。"
"在调用高风险操作前,先说明你的判断和方案。\n\n"
f"[执行计划]\n{json.dumps(plan, ensure_ascii=False, indent=2)}"
)
if memory_text:
system_content += f"\n\n[历史经验参考]\n{memory_text}"
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": user_input}
]
max_iterations = 10
for i in range(max_iterations):
print(f"\n--- 循环第 {i+1} 轮 ---")
messages = compress_messages(messages)
response = client.chat.completions.create(
model=model_name, messages=messages, tools=tools
)
msg = response.choices[0].message
if msg.tool_calls:
messages.append(msg)
for tool_call in msg.tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f"🔵 调用工具: {name},参数: {args}")
result = execute_tool(name, args)
print(f"🟢 工具返回: {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
print("🤖 Agent 生成最终回复")
memory.store(
f"{datetime.now().strftime('%Y-%m-%d')} 任务:{user_input}。结论:{msg.content[:200]}",
tags=[t.lower() for t in user_input.split() if len(t) > 2]
)
return msg.content
return "达到最大迭代次数,任务未完成。"
if __name__ == "__main__":
print("=" * 50)
print("Agent v4 --- 带规划 + 长期记忆 + 上下文压缩")
print("=" * 50)
result = run_agent("订单服务报错了,错误率飙升,帮我诊断并修复")
print("\n====================")
print("最终输出:")
print(result)
latex
运行效果(关注执行计划的生成):
==================================================
Agent v4 --- 带规划 + 长期记忆 + 上下文压缩
==================================================
当前使用的模型: gpt-4o | API地址: 官方默认
🧠 长期记忆检索:找到 1 条相关记忆
📋 正在生成执行计划...
📋 执行计划:
分析:订单服务错误率飙升,历史经验显示上次是数据库锁争用。
优先从数据库方向排查。
Step 1: 查询订单服务最近的日志,确认错误类型 (工具: query_logs)
原因: 先确认错误类型,判断排查方向
Step 2: 查询订单服务的监控指标 (工具: query_metrics)
原因: 获取量化数据,确认影响范围
Step 3: 根据日志分析结果,如果是数据库问题则终止阻塞查询 (工具: kill_query)
原因: 参考历史经验,锁争用是最可能的根因
Step 4: 验证服务恢复 (工具: query_metrics)
原因: 确认修复效果
--- 循环第 1 轮 ---
🔵 调用工具: query_logs,参数: {'service': 'order-service', 'time_range': 'last_30min'}
🟢 工具返回: [日志] order-service 在 last_30min 内有大量数据库连接超时错误
--- 循环第 2 轮 ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 3 轮 ---
🔵 调用工具: kill_query,参数: {'query_id': '8837'}
🟢 工具返回: [执行] 已终止查询 8837,锁已释放
--- 循环第 4 轮 ---
🔵 调用工具: query_metrics,参数: {'service': 'order-service'}
🟢 工具返回: [监控] order-service 当前错误率 15%,P99 延迟 8.2s
--- 循环第 5 轮 ---
🤖 Agent 生成最终回复
💾 长期记忆:已存储 → 2026-03-20 任务:订单服务报错了...
====================
最终输出:
根据诊断,订单服务错误率飙升的根因是数据库锁争用...
7.4 四个版本的演进对比
latex
┌────────────────────────────────────────────────────────────────┐
│ Agent 四个版本的演进 │
│ │
│ 版本 新增能力 解决的问题 │
│ ───────────────────────────────────────────────────────────── │
│ v1 (第一篇) 基础 Agent 循环 能不能自主调用工具 │
│ v2 (本篇) 工作记忆管理 会不会"健忘" │
│ v3 (本篇) 长期记忆 (简易 RAG) 能不能借鉴历史经验 │
│ v4 (本篇) 任务规划 会不会"瞎想" │
│ │
│ v1 → v2: 消息太多时自动压缩,不再浪费 Token │
│ v2 → v3: 遇到问题先查历史记忆,不再从零开始 │
│ v3 → v4: 先出计划再执行,不再边走边瞎摸 │
│ │
│ 后续文章将继续演进: │
│ v5 (第3篇): 多 Agent 协作 │
│ v6 (第4篇): 安全防护(注入防御 + 熔断) │
│ v7 (第5篇): 评测与可观测性 │
│ │
└────────────────────────────────────────────────────────────────┘
8. 本篇核心概念速查表
| 概念 | 一句话定义 | 解决的问题 | 实现方式 |
|---|---|---|---|
| 工作记忆 | LLM 的上下文窗口 | 本次任务的短期信息管理 | 滑动窗口 / 摘要压缩 / 重要性过滤 |
| 长期记忆 | 跨任务持久化的外部存储 | 借鉴历史经验 | 向量数据库 + 语义检索(RAG) |
| Scratchpad | 结构化的执行状态记录 | 知道做到哪一步了 | JSON 格式注入系统 Prompt |
| RAG | 检索增强生成 | 让 LLM 能利用外部知识 | 检索 → 注入 → 生成 |
| 无规划(ReAct) | 边想边做 | 简单任务的灵活应对 | Thought → Action → Observation |
| Plan-then-Execute | 先想后做 | 复杂任务的全局视角 | 生成计划 → 逐步执行 |
| 动态重规划 | 走的过程中修正地图 | 计划与现实不符 | 异常触发 → 重新规划 |
| 反思(Reflexion) | 做了再改 | 从失败中学习 | 执行 → 反思 → 存入记忆 → 改进重试 |
| 上下文工程 | 有限 Token 内的信息排兵布阵 | Token 预算有限 | 组装配方 + 取舍策略 |
9. 小结
本篇解决了 Agent 的三大顽疾:健忘 (通过工作记忆管理和长期记忆)、死循环 (通过错误分析和策略调整)、瞎想(通过任务规划和 Scratchpad)。
我们把 Agent 的记忆分为三层:工作记忆(上下文窗口,管"正在想什么")、长期记忆(外部存储,管"以前遇到过什么")、工作台记忆(Scratchpad,管"做到哪一步了")。三层记忆各司其职,共同支撑 Agent 在复杂任务中的推理能力。
在长期记忆的实现上,我们介绍了 RAG(检索增强生成)作为 Agent "外部大脑"的角色,并澄清了 RAG 和 Agent 的关系------它们是不同层面的概念,可以独立存在,也可以互补使用。RAG 的完整工程化将在 RAG 工程化指南系列中详细展开。
在规划层面,我们对比了三种范式:无规划(ReAct 的边走边看)、先规划后执行(全局视角但缺乏灵活性)、动态重规划(在执行中修正计划)。实际工程中,这三种范式往往组合使用。
最后,通过四个版本的递进式代码演进(v1 基础 Agent → v2 工作记忆 → v3 长期记忆 → v4 任务规划),我们把抽象的记忆和规划机制转化为了可运行的代码。读者手中的 Agent 从"能调工具"进化到了"记得住历史、做得出计划"。
接下来的问题是:当任务复杂到单个 Agent 搞不定时怎么办? 如何让多个 Agent 分工协作------一个负责告警接收,一个负责数据库诊断,一个负责生成报告?这就是下一篇的主题。
下一篇预告:《Agent 工程化指南(三):编排与多智能体 --- 从单兵作战到 Agent 团队》
参考来源
- Park, J.S. et al. (2023). "Generative Agents: Interactive Simulacra of Human Behavior". 斯坦福大学的研究,创建了 25 个基于 LLM 的生成式智能体小镇。论文中提出了 Agent 记忆流(Memory Stream)的概念,包括情景记忆和语义记忆的区分,以及基于近因性(Recency)、重要性(Importance)和相关性(Relevance)的记忆检索机制。参见 arXiv:2304.03442。
- 上下文窗口管理 --- LLM 的上下文窗口(Context Window)是指模型在单次推理中能处理的最大 Token 数量。不同模型的窗口大小不同(如 GPT-4o 为 128K tokens,Claude 3.5 为 200K tokens)。超出窗口限制的信息会被截断。参见 OpenAI Docs: Token Limits。上下文压缩(Summarization)和滑动窗口(Sliding Window)是社区广泛采用的管理策略,在 LangChain、LlamaIndex 等框架中有标准化实现。
- Zhong, W. et al. (2024). "MemoryBank: Enhancing Large Language Models with Long-Term Memory". 提出了 MemoryBank 框架,让 LLM 拥有长期记忆能力。系统会存储用户的历史交互记录,并根据近因性逐渐遗忘不重要的记忆,类似于 Ebbinghaus 遗忘曲线。参见 arXiv:2305.10250。
- Scratchpad / 工作台记忆 --- "Scratchpad"一词最早出现在 Nye et al. (2021) 的论文 "Show Your Work: Scratchpads for Intermediate Computation with Language Models" 中,指在 Prompt 中为 LLM 提供的中间计算空间。在 Agent 场景中,Scratchpad 被扩展为记录任务执行的中间状态。参见 arXiv:2112.00114。
- Lewis, P. et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks". 提出了 RAG(检索增强生成)框架:在生成回答之前,先从外部知识库中检索相关文档,将检索结果注入到 LLM 的输入中,从而增强生成质量。这是 RAG 技术的奠基论文。参见 arXiv:2005.11401。
- MCP(Model Context Protocol) --- Anthropic 于 2024 年提出的开放协议,旨在标准化 LLM 应用与外部工具、数据源之间的连接方式。MCP 定义了 Server/Client 架构、工具注册与发现机制、资源管理等规范。参见 MCP 官方文档。
- Wang, L. et al. (2023). "Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models". 提出了 Plan-and-Solve 提示策略,让 LLM 在解答前先制定计划。在 Agent 场景中,这种思路被扩展为 Plan-then-Execute 架构模式。参见 arXiv:2305.04091。
- 规划与重规划 --- Agent 领域的规划范式源自经典 AI 规划(AI Planning)的研究传统。在 LLM Agent 中,Plan-then-Execute 模式通过 LLM 生成自然语言计划,然后逐步执行。LangChain 的 Plan-and-Execute Agent 和 BabyAGI 是这种模式的典型实现。动态重规划(Re-planning)在 Wang et al. (2023) 的后续工作和 LangGraph 的条件路由机制中有具体体现。参见 LangGraph 文档。
- Shinn, N. et al. (2023). "Reflexion: Language Agents with Verbal Reinforcement Learning". 提出了 Reflexion 框架,Agent 通过执行→反思→改进的循环来提升任务表现。反思结论以自然语言形式存入记忆,在后续尝试中被检索和利用。发表于 NeurIPS 2023。参见 arXiv:2303.11366。