Agent 工程化指南(二):记忆与规划 — 让 Agent “记得住“且“做得对“

一句话定位:解决 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 团队》


参考来源

  1. Park, J.S. et al. (2023). "Generative Agents: Interactive Simulacra of Human Behavior". 斯坦福大学的研究,创建了 25 个基于 LLM 的生成式智能体小镇。论文中提出了 Agent 记忆流(Memory Stream)的概念,包括情景记忆和语义记忆的区分,以及基于近因性(Recency)、重要性(Importance)和相关性(Relevance)的记忆检索机制。参见 arXiv:2304.03442
  2. 上下文窗口管理 --- LLM 的上下文窗口(Context Window)是指模型在单次推理中能处理的最大 Token 数量。不同模型的窗口大小不同(如 GPT-4o 为 128K tokens,Claude 3.5 为 200K tokens)。超出窗口限制的信息会被截断。参见 OpenAI Docs: Token Limits。上下文压缩(Summarization)和滑动窗口(Sliding Window)是社区广泛采用的管理策略,在 LangChain、LlamaIndex 等框架中有标准化实现。
  3. Zhong, W. et al. (2024). "MemoryBank: Enhancing Large Language Models with Long-Term Memory". 提出了 MemoryBank 框架,让 LLM 拥有长期记忆能力。系统会存储用户的历史交互记录,并根据近因性逐渐遗忘不重要的记忆,类似于 Ebbinghaus 遗忘曲线。参见 arXiv:2305.10250
  4. Scratchpad / 工作台记忆 --- "Scratchpad"一词最早出现在 Nye et al. (2021) 的论文 "Show Your Work: Scratchpads for Intermediate Computation with Language Models" 中,指在 Prompt 中为 LLM 提供的中间计算空间。在 Agent 场景中,Scratchpad 被扩展为记录任务执行的中间状态。参见 arXiv:2112.00114
  5. Lewis, P. et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks". 提出了 RAG(检索增强生成)框架:在生成回答之前,先从外部知识库中检索相关文档,将检索结果注入到 LLM 的输入中,从而增强生成质量。这是 RAG 技术的奠基论文。参见 arXiv:2005.11401
  6. MCP(Model Context Protocol) --- Anthropic 于 2024 年提出的开放协议,旨在标准化 LLM 应用与外部工具、数据源之间的连接方式。MCP 定义了 Server/Client 架构、工具注册与发现机制、资源管理等规范。参见 MCP 官方文档
  7. 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
  8. 规划与重规划 --- Agent 领域的规划范式源自经典 AI 规划(AI Planning)的研究传统。在 LLM Agent 中,Plan-then-Execute 模式通过 LLM 生成自然语言计划,然后逐步执行。LangChain 的 Plan-and-Execute Agent 和 BabyAGI 是这种模式的典型实现。动态重规划(Re-planning)在 Wang et al. (2023) 的后续工作和 LangGraph 的条件路由机制中有具体体现。参见 LangGraph 文档
  9. Shinn, N. et al. (2023). "Reflexion: Language Agents with Verbal Reinforcement Learning". 提出了 Reflexion 框架,Agent 通过执行→反思→改进的循环来提升任务表现。反思结论以自然语言形式存入记忆,在后续尝试中被检索和利用。发表于 NeurIPS 2023。参见 arXiv:2303.11366
相关推荐
森叶15 小时前
从 MCP 到扣子:Agent 平台化演化路径
人工智能·大模型·agent·ai agent·mcp
Darling噜啦啦16 小时前
远程MCP实战:高德地图 + Chrome DevTools + File System 三大 Server 联动打造 AI 超级 Agent
mcp
还是转转17 小时前
深入认识 Agent - 构建简单的 Agent 框架
ai·agent·函数调用
Z-D-K21 小时前
AI对Anthropic的J-space的分析(1)
ai·aigc·人机交互·agent·agi
沉默王二1 天前
Hy3 + WorkBuddy = 国产顶级 Agent(附完整提示词)。
agent·ai编程·腾讯
不好听6131 天前
MCP Resources:让 AI Agent 拥有"知识库"
agent
浮生望1 天前
MCP 协议:打破工具围墙,让 AI Agent 跨语言跨进程调用任意工具
agent·mcp
凛风前行1 天前
多智能体架构的 5 种经典模式
架构·agent
leeyi1 天前
调试 Agent 的 3 个姿势:日志、断点、可视化(第49篇-E35)
aigc·agent·devops