---
title: "Agent_01_Agent核心架构"
source: https://www.zbhgis.com/blog/blog-20260103-1
author: zbhgis 浩瀚地学
created: 2026-01-02
tags: [Agent, agent, Python]
---

> 本文来自浩瀚地学，规范页面（含代码高亮与图片灯箱）：https://www.zbhgis.com/blog/blog-20260103-1
# Agent_01_Agent核心架构

最简定义：

> **Agent = LLM（大脑）+ 记忆（Memory）+ 工具（Tools）+ 规划（Planning）**

这四件事，每一件都可以单独拿出来优化、替换、调试。很多人觉得 Agent"不稳定"、"不可靠"，本质上是因为把这四件事混在一起写，出了问题不知道是记忆漏了、工具错了、还是规划乱了。

先看一个常见的反面做法——把所有逻辑塞进一个 prompt：

```python
# 反例：一个 prompt 干所有事
system_prompt = """
你是一个数据分析助手。你需要：
1. 记住用户的偏好（之前说过喜欢柱状图而不是折线图）
2. 如果用户提到数据，用 pandas 读取 csv 文件
3. 如果用户要求画图，用 matplotlib 画图
4. 如果用户的问题不清楚，先问清楚再行动
5. 最后输出分析报告
"""

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "system", "content": system_prompt},
              {"role": "user", "content": user_input}]
)
```

这段代码的问题很明显：没有真正的工具调用（模型只能"告诉"你该做什么，但做不到）；记忆是假的（每次调用都是独立请求，上一轮的"偏好"下一轮就没了）；五个要求写在一个 prompt 里，模型不知道哪个先做哪个后做；输出不对的时候，你不知道是 prompt 写得不好、模型理解错了、还是该给模型加个工具。

把这四件事拆开，每件交给专门的组件处理：

```
┌─────────────────────────────────────────────────┐
│                   Agent Loop                     │
│                                                  │
│   ┌──────────┐                                   │
│   │  规划     │ ← 用户目标: "分析这份销售数据"    │
│   │ Planner  │   → 拆解: 读数据 → 清洗 → 分析    │
│   └────┬─────┘                                   │
│        │ 执行步骤                                  │
│   ┌────▼─────┐    ┌──────────┐                   │
│   │  工具     │    │  记忆     │                   │
│   │  Tools   │◄──►│ Memory   │                   │
│   │ read_csv │    │ 偏好:柱状图│                   │
│   │ analyze  │    │ 历史:Q3   │                   │
│   │ plot     │    └──────────┘                   │
│   └────┬─────┘                                   │
│        │ 结果                                     │
│   ┌────▼─────┐                                   │
│   │   LLM    │ ← 综合工具和记忆的信息，生成回复   │
│   │ (大脑)   │                                   │
│   └──────────┘                                   │
└─────────────────────────────────────────────────┘
```

模型还是那个模型，但给了它记忆让它能回忆、给了它工具让它能动手、给了它规划让它知道先做什么后做什么。Agent 不是"更聪明的 LLM"，而是"更有组织的 LLM"。

---

## 最简 Agent：四大组件齐全

这个例子展示了最精简但完整的 Agent 架构。每个组件独立、可替换、可调试。

```python
"""
最简 Agent 架构 —— LLM + Memory + Tools + Planner
每个组件都是独立接口，可以随时替换实现。
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any


class LLM(ABC):
    """LLM 接口 —— 所有模型实现这个契约"""

    @abstractmethod
    def generate(self, prompt: str, system: str = "", **kwargs) -> str:
        ...

    @abstractmethod
    def generate_with_tools(
        self, prompt: str, tools: list[dict], system: str = "", **kwargs
    ) -> tuple[str, list[dict]]:
        """返回 (文本, 工具调用列表)。工具调用格式: [{"name": "xxx", "args": {...}}]"""
        ...


class OpenAILLM(LLM):
    """OpenAI 模型实现"""

    def __init__(self, model: str = "gpt-4o-mini", api_key: str = ""):
        self.model = model
        self.api_key = api_key

    def generate(self, prompt: str, system: str = "", **kwargs) -> str:
        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})
        # 实际场景：调用 openai SDK
        # response = openai.ChatCompletion.create(model=self.model, messages=messages)
        # return response.choices[0].message.content
        print(f"[LLM] 调用 {self.model}: {prompt[:80]}...")
        return "(模拟回复)"

    def generate_with_tools(
        self, prompt: str, tools: list[dict], system: str = "", **kwargs
    ) -> tuple[str, list[dict]]:
        print(f"[LLM] 调用 {self.model} (带工具): {prompt[:80]}...")
        return ("我帮你分析数据", [{"name": "read_csv", "args": {"path": "data.csv"}}])


class Memory(ABC):
    """记忆接口 —— 管读写"""

    @abstractmethod
    def add(self, key: str, value: Any) -> None:
        ...

    @abstractmethod
    def get(self, key: str) -> Any | None:
        ...

    @abstractmethod
    def get_all(self) -> dict:
        ...

    @abstractmethod
    def clear(self) -> None:
        ...


class DictMemory(Memory):
    """最简单的内存 —— 就是一个字典"""

    def __init__(self, max_size: int = 50):
        self._store: dict[str, Any] = {}
        self.max_size = max_size

    def add(self, key: str, value: Any) -> None:
        if len(self._store) >= self.max_size:
            oldest_key = next(iter(self._store))
            del self._store[oldest_key]
        self._store[key] = value

    def get(self, key: str) -> Any | None:
        return self._store.get(key)

    def get_all(self) -> dict:
        return dict(self._store)

    def clear(self) -> None:
        self._store.clear()

    def __repr__(self):
        return f"DictMemory({self._store})"


@dataclass
class ToolResult:
    """工具执行结果"""
    success: bool
    output: Any
    error: str = ""


class ToolRegistry:
    """工具注册表 —— 管着所有可用工具"""

    def __init__(self):
        self._tools: dict[str, Any] = {}

    def register(self, name: str, func: Any, description: str = "") -> None:
        self._tools[name] = {"func": func, "description": description}

    def execute(self, name: str, **kwargs) -> ToolResult:
        if name not in self._tools:
            return ToolResult(success=False, output=None, error=f"未知工具: {name}")
        try:
            result = self._tools[name]["func"](**kwargs)
            return ToolResult(success=True, output=result)
        except Exception as e:
            return ToolResult(success=False, output=None, error=str(e))

    def get_schemas(self) -> list[dict]:
        """返回所有工具的 schema，给 LLM 看"""
        return [
            {"name": name, "description": info["description"]}
            for name, info in self._tools.items()
        ]


class Planner(ABC):
    """规划接口 —— 把目标拆成步骤"""

    @abstractmethod
    def plan(self, goal: str, context: str = "") -> list[str]:
        """返回步骤列表"""
        ...


class SimplePlanner(Planner):
    """最简规划器 —— 用 LLM 生成步骤"""

    def __init__(self, llm: LLM):
        self._llm = llm

    def plan(self, goal: str, context: str = "") -> list[str]:
        system = (
            "你是一个任务规划专家。把用户的目标拆解为可执行的步骤。"
            "每步用一句话描述，返回列表格式。"
        )
        prompt = f"目标：{goal}\n上下文：{context}\n请拆解为步骤："
        result = self._llm.generate(prompt, system=system)
        return [s.strip() for s in result.split("\n") if s.strip()]


@dataclass
class AgentConfig:
    """Agent 配置"""
    max_steps: int = 10
    verbose: bool = True


class Agent:
    """
    Agent 主循环 —— 接收目标 → 规划 → 逐步执行 → 返回结果
    """

    def __init__(
        self,
        llm: LLM,
        memory: Memory,
        tools: ToolRegistry,
        planner: Planner,
        config: AgentConfig = None,
    ):
        self._llm = llm
        self._memory = memory
        self._tools = tools
        self._planner = planner
        self._config = config or AgentConfig()

    def run(self, goal: str) -> str:
        # 规划
        context = ", ".join(f"{k}={v}" for k, v in self._memory.get_all().items())
        steps = self._planner.plan(goal, context=context)

        if self._config.verbose:
            print(f"=== 规划完成，共 {len(steps)} 步 ===")
            for i, step in enumerate(steps, 1):
                print(f"  {i}. {step}")

        # 执行循环
        for step_idx, step in enumerate(steps, 1):
            if step_idx > self._config.max_steps:
                return "达到最大步数限制，任务未完成。"

            if self._config.verbose:
                print(f"\n--- 执行第 {step_idx} 步: {step} ---")

            tool_schemas = self._tools.get_schemas()
            thought, tool_calls = self._llm.generate_with_tools(
                prompt=f"当前步骤: {step}\n可用工具: {tool_schemas}",
                tools=tool_schemas,
            )

            if self._config.verbose:
                print(f"  思考: {thought}")

            for tool_call in tool_calls:
                name, args = tool_call["name"], tool_call.get("args", {})
                result = self._tools.execute(name, **args)

                if self._config.verbose:
                    status = "成功" if result.success else f"失败: {result.error}"
                    print(f"  工具 {name}: {status}")

                self._memory.add(f"step_{step_idx}_{name}", result.output)

        # 综合输出
        all_results = self._memory.get_all()
        final_prompt = (
            f"目标: {goal}\n"
            f"执行结果: {all_results}\n"
            f"请生成一份完整的回复。"
        )
        return self._llm.generate(final_prompt, system="你是数据分析助手")


def main():
    llm = OpenAILLM(model="gpt-4o-mini")
    memory = DictMemory(max_size=50)
    tools = ToolRegistry()
    planner = SimplePlanner(llm)

    def read_csv(path: str) -> dict:
        import pandas as pd
        df = pd.read_csv(path)
        return {
            "shape": df.shape,
            "columns": list(df.columns),
            "head": df.head(3).to_dict(),
            "dtypes": {col: str(dt) for col, dt in df.dtypes.items()},
        }

    def analyze(df_data: dict) -> dict:
        return {"summary": "数据正常，无异常值", "trends": "Q4 增长 15%"}

    tools.register("read_csv", read_csv, "读取 CSV 文件，返回数据结构")
    tools.register("analyze", analyze, "分析数据，返回摘要和趋势")

    memory.add("user_preference", "喜欢柱状图")

    agent = Agent(llm, memory, tools, planner, config=AgentConfig(verbose=True))
    result = agent.run("分析这份销售数据，告诉我 Q4 的表现")
    print(f"\n=== 最终结果 ===\n{result}")


if __name__ == "__main__":
    main()
```

几个注意点：

- LLM 接口可以随时从 OpenAI 换成 Claude、本地模型，只要实现 `generate` 和 `generate_with_tools` 两个方法
- Memory 现在是字典，换成 Redis、SQLite、向量数据库都行
- ToolRegistry 是注册表模式，新增工具只需要 `tools.register(name, func, desc)` 一行
- Planner 现在用 LLM 规划，可以换成硬编码步骤、或者让 LLM 输出 JSON 格式的步骤列表
- Agent 主循环核心逻辑不到 30 行——规划、循环执行、综合输出

## 对话历史记忆

上面的 `DictMemory` 只管 key-value，但 Agent 真正的记忆是对话历史。

```python
class ChatMemory(Memory):
    """对话记忆 —— 管着完整的对话历史"""

    def __init__(self, max_tokens: int = 4000):
        self._messages: list[dict] = []
        self.max_tokens = max_tokens

    def add(self, key: str, value: Any) -> None:
        if isinstance(value, dict):
            self._messages.append(value)
        else:
            self._messages.append({"role": "user", "content": str(value)})

    def get(self, key: str) -> Any | None:
        for msg in reversed(self._messages):
            if key.lower() in msg.get("content", "").lower():
                return msg
        return None

    def get_all(self) -> dict:
        return {"messages": self._messages}

    def clear(self) -> None:
        self._messages.clear()

    def get_messages(self) -> list[dict]:
        return list(self._messages)

    def truncate_if_needed(self) -> list[dict]:
        total_chars = sum(len(msg.get("content", "")) for msg in self._messages)
        if total_chars <= self.max_tokens * 4:
            return self._messages

        trimmed = list(self._messages)
        while trimmed and total_chars > self.max_tokens * 4:
            removed = trimmed.pop(0)
            total_chars -= len(removed.get("content", ""))
        return trimmed

    def __repr__(self):
        return f"ChatMemory({len(self._messages)} messages)"


memory = ChatMemory(max_tokens=4000)
memory.add("", {"role": "user", "content": "我喜欢柱状图"})
memory.add("", {"role": "assistant", "content": "好的，记住了。以后用柱状图展示。"})
memory.add("", {"role": "user", "content": "分析这份数据"})

messages = memory.get_messages()
```

`max_tokens` 不是随便设的。你需要知道模型的上下文窗口有多大（GPT-4 是 8K 或 32K，Claude 是 200K），然后留出空间给 system prompt 和输出。设太小会丢失上下文，设太大会浪费 token 且可能让模型注意力分散。至少我目前的做法是设到窗口上限的 70% 左右，留 30% 给 system prompt 和输出。

## 工具的错误处理

工具执行一定会失败——文件不存在、API 超时、返回格式不对。

```python
import time
from functools import wraps


class ToolError(Exception):
    """工具执行异常"""
    pass


def with_retry(max_retries: int = 3, delay: float = 1.0):
    """重试装饰器 —— 工具失败自动重试"""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    if attempt < max_retries - 1:
                        print(f"  工具 {func.__name__} 第 {attempt + 1} 次失败: {e}，{delay}s 后重试...")
                        time.sleep(delay)
            raise ToolError(f"{func.__name__} 重试 {max_retries} 次后仍失败: {last_error}")

        return wrapper

    return decorator


@with_retry(max_retries=3, delay=2.0)
def call_api(endpoint: str) -> dict:
    import random
    if random.random() < 0.5:
        raise ConnectionError("Connection refused")
    return {"data": "success"}
```

工具重试是 Agent 里最实用的基础设施之一。网络调用、外部 API、文件 IO 都靠它兜底。重试次数和间隔需要根据具体场景调——外部 API 通常 3 次、间隔 2 秒就够了；本地文件操作失败通常重试也没用，可以设 1 次。

## 三层记忆架构

`DictMemory` 和 `ChatMemory` 都只解决了短期记忆——当前对话窗口内的信息。但 Agent 真正的记忆需求分三层：

| 层级 | 对应 | 存储 | 生命周期 | 检索方式 |
|------|------|------|---------|---------|
| **短期记忆** | 工作记忆 | 内存列表 | 当前会话 | 顺序/关键词 |
| **长期记忆** | 用户偏好、历史经验 | 文件/数据库 | 持久化 | key-value 查询 |
| **语义记忆** | "上次做过类似的事" | 向量数据库 | 持久化 | 向量相似度 |

```python
"""
三层记忆架构 —— 短期（上下文）+ 长期（持久化）+ 语义（向量检索）
"""

import json
import os
import math
from abc import ABC, abstractmethod
from typing import Any


class ShortTermMemory:
    """短期记忆 —— 当前会话的对话历史"""

    def __init__(self, max_messages: int = 50):
        self._messages: list[dict] = []
        self.max_messages = max_messages

    def add(self, role: str, content: str) -> None:
        self._messages.append({"role": role, "content": content})
        while len(self._messages) > self.max_messages:
            self._messages.pop(0)

    def get_recent(self, n: int = 10) -> list[dict]:
        return self._messages[-n:]

    def summarize(self) -> str:
        return " | ".join(
            f"{m['role']}: {m['content'][:50]}" for m in self._messages[-5:]
        )

    def to_messages(self) -> list[dict]:
        return list(self._messages)


class LongTermMemory:
    """长期记忆 —— 持久化的 key-value 存储"""

    def __init__(self, file_path: str = "memory.json"):
        self.file_path = file_path
        self._store: dict[str, Any] = self._load()

    def _load(self) -> dict:
        if os.path.exists(self.file_path):
            with open(self.file_path, "r", encoding="utf-8") as f:
                return json.load(f)
        return {}

    def _save(self) -> None:
        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump(self._store, f, ensure_ascii=False, indent=2)

    def set(self, key: str, value: Any) -> None:
        self._store[key] = value
        self._save()

    def get(self, key: str, default=None) -> Any:
        return self._store.get(key, default)

    def delete(self, key: str) -> None:
        self._store.pop(key, None)
        self._save()

    def get_facts(self) -> str:
        if not self._store:
            return "（无已知事实）"
        return "\n".join(f"- {k}: {v}" for k, v in self._store.items())


class SemanticMemory:
    """
    语义记忆 —— 基于向量相似度的检索

    这里用简化的 TF-IDF 风格实现（实际应该用 embedding 模型）。
    """

    def __init__(self, top_k: int = 3):
        self._entries: list[dict] = []
        self.top_k = top_k

    def store(self, text: str, metadata: dict = None) -> None:
        self._entries.append({"text": text, "metadata": metadata or {}})

    def retrieve(self, query: str) -> list[dict]:
        if not self._entries:
            return []

        query_tokens = set(self._tokenize(query))

        scored = []
        for entry in self._entries:
            entry_tokens = set(self._tokenize(entry["text"]))
            overlap = len(query_tokens & entry_tokens)
            union = len(query_tokens | entry_tokens)
            score = overlap / union if union > 0 else 0
            scored.append((score, entry))

        scored.sort(key=lambda x: x[0], reverse=True)
        return [
            {"text": entry["text"], "metadata": entry["metadata"], "score": score}
            for score, entry in scored[: self.top_k]
        ]

    def _tokenize(self, text: str) -> list[str]:
        return text.lower().split()


class CompositeMemory:
    """组合记忆 —— 统一管理三层记忆"""

    def __init__(self):
        self.short_term = ShortTermMemory(max_messages=50)
        self.long_term = LongTermMemory(file_path="memory.json")
        self.semantic = SemanticMemory(top_k=3)

    def add_message(self, role: str, content: str, important: bool = False) -> None:
        self.short_term.add(role, content)
        if important:
            self.long_term.set(f"msg_{len(self.short_term._messages)}", content)
            self.semantic.store(content, {"role": role, "timestamp": "now"})

    def build_context(self, query: str = "") -> str:
        parts = []

        recent = self.short_term.get_recent(5)
        if recent:
            parts.append("### 最近对话")
            for msg in recent:
                parts.append(f"{msg['role']}: {msg['content']}")

        facts = self.long_term.get_facts()
        if facts:
            parts.append("### 已知事实")
            parts.append(facts)

        if query:
            related = self.semantic.retrieve(query)
            if related:
                parts.append("### 相关历史")
                for r in related:
                    parts.append(f"[相关度 {r['score']:.2f}] {r['text']}")

        return "\n\n".join(parts)


memory = CompositeMemory()

memory.add_message("user", "我喜欢用柱状图展示数据")
memory.add_message("assistant", "好的，已记住你的偏好")

memory.add_message("user", "我的数据源是 sales_2025.csv", important=True)

context = memory.build_context(query="数据分析")
print(context)
```

三层记忆不是"存越多越好"，每层有不同的访问频率和检索策略。短期记忆是顺序访问（最近的最重要），长期记忆是 key-value 查找（精确匹配），语义记忆是相似度检索（模糊匹配）。把它们混在一个类里，检索效率会急剧下降。实际项目中，我见过很多人把所有东西塞进一个向量库，结果每次查询都要跑 embedding，慢不说，精确查询反而不如直接查字典。

## 从函数签名自动生成 Tool Schema

前面工具 schema 是手动写的 `description`。实际项目中工具可能有几十个，手动维护容易出错。下面的代码展示如何从 Python 函数签名自动生成 JSON Schema：

```python
"""
从函数签名自动生成 Tool Schema
"""

import inspect
import json
from typing import get_type_hints


def generate_tool_schema(func) -> dict:
    """
    从 Python 函数自动生成工具 schema

    提取：函数名、docstring（作为描述）、参数名、参数类型、默认值、必填项
    """
    sig = inspect.signature(func)
    hints = get_type_hints(func)

    properties = {}
    required = []

    for name, param in sig.parameters.items():
        if name == "self":
            continue

        param_type = hints.get(name, Any)
        type_mapping = {
            str: "string",
            int: "integer",
            float: "number",
            bool: "boolean",
            list: "array",
            dict: "object",
        }
        json_type = type_mapping.get(param_type, "string")

        prop = {"type": json_type}

        docstring = inspect.getdoc(func) or ""
        for line in docstring.split("\n"):
            line = line.strip()
            if line.startswith(f":param {name}:"):
                prop["description"] = line.split(":", 2)[-1].strip()

        if param.default is inspect.Parameter.empty:
            required.append(name)
        else:
            prop["default"] = param.default

        properties[name] = prop

    func_desc = (inspect.getdoc(func) or "").split("\n")[0].strip()

    return {
        "name": func.__name__,
        "description": func_desc,
        "parameters": {
            "type": "object",
            "properties": properties,
            "required": required,
        },
    }


def read_csv(path: str, delimiter: str = ",", encoding: str = "utf-8") -> dict:
    """
    读取 CSV 文件并返回数据结构

    :param path: CSV 文件路径
    :param delimiter: 分隔符，默认逗号
    :param encoding: 文件编码，默认 utf-8
    """
    return {"path": path, "rows": 100, "columns": 5}


def analyze_data(data: dict, metrics: list = None) -> dict:
    """
    分析数据并返回统计摘要

    :param data: 待分析的数据字典
    :param metrics: 要计算的指标列表
    """
    return {"mean": 0.5, "std": 0.1}


schema1 = generate_tool_schema(read_csv)
schema2 = generate_tool_schema(analyze_data)

print(json.dumps(schema1, indent=2, ensure_ascii=False))
# 输出：
# {
#   "name": "read_csv",
#   "description": "读取 CSV 文件并返回数据结构",
#   "parameters": {
#     "type": "object",
#     "properties": {
#       "path": {"type": "string", "description": "CSV 文件路径"},
#       "delimiter": {"type": "string", "description": "分隔符，默认逗号", "default": ","},
#       "encoding": {"type": "string", "description": "文件编码，默认 utf-8", "default": "utf-8"}
#     },
#     "required": ["path"]
#   }
# }
```

这种做法消除了手动维护 schema 和实际函数签名不一致的问题。改了函数参数，schema 自动更新。工具数量超过 10 个的时候，手动维护的 schema 一定会和实际函数不同步——我踩过这个坑。

## 上下文窗口管理

Agent 运行过程中，上下文（对话历史 + 工具结果）不断增长，最终会触及模型的 token 限制。三种常见策略：

```python
"""
上下文窗口管理的三种策略

ReAct 循环每步追加 Thought + Action + Observation，
10 步之后上下文可能超过 4000 token（GPT-3.5 的限制）。
"""

from typing import Callable


class ContextWindowManager:
    """上下文管理器 —— 监控和控制 token 使用量"""

    def __init__(self, max_tokens: int = 4000):
        self.max_tokens = max_tokens
        self._current_tokens: int = 0

    def estimate_tokens(self, text: str) -> int:
        english_chars = sum(1 for c in text if c.isascii())
        chinese_chars = len(text) - english_chars
        return int(english_chars / 4 + chinese_chars / 1.5)

    def add(self, text: str) -> bool:
        token_count = self.estimate_tokens(text)
        if self._current_tokens + token_count > self.max_tokens:
            return False
        self._current_tokens += token_count
        return True

    @property
    def usage_ratio(self) -> float:
        return self._current_tokens / self.max_tokens


class SlidingWindowStrategy:
    """
    滑动窗口 —— 只保留最近的 N 条消息

    适用场景：早期步骤的信息不再重要。
    优点：简单、token 使用量恒定。
    缺点：可能丢掉重要的早期信息。
    """

    def __init__(self, keep_recent: int = 10):
        self.keep_recent = keep_recent

    def compress(self, messages: list[dict], system_prompt: str = "") -> str:
        recent = messages[-self.keep_recent:]
        parts = [system_prompt] if system_prompt else []
        for msg in recent:
            parts.append(f"{msg['role']}: {msg['content']}")
        return "\n".join(parts)


class SummaryCompressionStrategy:
    """
    摘要压缩 —— 用 LLM 把早期步骤压缩成一段摘要

    适用场景：早期步骤有重要信息，但不需要保留完整细节。
    优点：保留关键信息、大幅减少 token。
    缺点：需要额外一次 LLM 调用；摘要可能丢失细节。
    """

    def __init__(self, llm, compress_trigger: float = 0.7):
        self.llm = llm
        self.compress_trigger = compress_trigger

    def should_compress(self, window: ContextWindowManager) -> bool:
        return window.usage_ratio > self.compress_trigger

    def compress(self, messages: list[dict], system_prompt: str = "") -> str:
        early_messages = messages[:-5]
        early_text = "\n".join(f"{m['role']}: {m['content']}" for m in early_messages)

        prompt = f"""
请将以下对话历史压缩为一段简要摘要。只保留关键信息：目标、工具调用结果、重要发现。
不要保留详细的 Thought 过程。

{early_text}
"""
        summary = self.llm.generate(prompt, system="你是对话摘要专家。")

        recent = messages[-5:]
        parts = [system_prompt, f"[早期对话摘要]\n{summary}"]
        for msg in recent:
            parts.append(f"{msg['role']}: {msg['content']}")
        return "\n".join(parts)


class KeyInfoExtractionStrategy:
    """
    关键信息提取 —— 每步执行后，只保留关键信息到上下文

    适用场景：工具输出很长（比如整个 CSV 内容），但只有少量信息有用。
    优点：从源头控制 token 增长，不需要事后压缩。
    缺点：需要为每个工具定义"什么信息是关键"的逻辑。
    """

    def __init__(self):
        self._extractors: dict[str, Callable] = {}

    def register_extractor(self, tool_name: str, extractor: Callable) -> None:
        self._extractors[tool_name] = extractor

    def process_observation(self, tool_name: str, raw_output: Any) -> str:
        if tool_name in self._extractors:
            return self._extractors[tool_name](raw_output)
        return str(raw_output)[:500]


extractor = KeyInfoExtractionStrategy()

def extract_csv_info(data: dict) -> str:
    return f"文件已读取: {data['rows']} 行 × {data['columns']} 列, 字段: {', '.join(data['columns'])}"

extractor.register_extractor("read_csv", extract_csv_info)

raw_output = {"rows": 10000, "columns": ["date", "revenue", "region"], "head": {...}}
observation = extractor.process_observation("read_csv", raw_output)
print(observation)
# 输出: "文件已读取: 10000 行 × 3 列, 字段: date, revenue, region"
```

根据上面的代码，三种策略各有取舍：滑动窗口 token 用量固定但会丢失早期信息，适合流水线任务；摘要压缩保留关键信息但多一次 LLM 调用成本，适合长对话；关键信息提取从源头控制 token 增长，但需要为每个工具写提取逻辑，适合工具输出很长的场景。实际项目中我通常会混合使用——滑动窗口兜底，关键信息提取控制工具输出，摘要压缩只在极端情况下触发。

## 一些实际问题

Agent 架构真的有必要吗？什么时候直接调 LLM 就够了？

如果你的任务就是问答、翻译、总结、改写，这些是 LLM 的本职工作，加 Agent 架构只会增加复杂度。不需要外部数据、不需要调用 API、不需要读写文件的时候，纯文本处理不需要工具。任务只有一两步（比如"把这段英文翻译成中文，然后总结要点"），也不需要规划。

反过来，任务需要多步操作且中间步骤依赖外部工具或数据、需要记住之前的对话或用户偏好、同一个任务不同用户的执行路径不同、需要追踪每一步的执行结果——这些场景才需要 Agent 架构。

四大组件里，哪个最容易出问题？我个人建议重点关注记忆和工具这两个。记忆管理是最容易被低估的组件——记太多上下文爆炸，记太少 Agent 变健忘。更深层的问题是遗忘策略：删哪条记忆？FIFO 太粗暴，LRU 好一点，但最好的策略是按"重要性"标记，只删不重要的。工具失败是 Agent 最常见的问题，API 超时、文件格式不对、参数不匹配，这些都不关 LLM 的事。更深层的问题是工具描述的准确性：如果 tool schema 的 description 写得不清楚，LLM 会选错工具或者传错参数。

Planner 是最容易被过度设计的组件。简单的任务不需要复杂的规划器，很多时候一个固定的步骤列表就够了。但复杂任务没有好的规划器，Agent 就会乱来。更深层的问题是规划与执行的 gap：LLM 规划的步骤工具可能不支持，工具支持的规划可能漏掉。

Agent 架构引入的代价也很实在：一次调用等于多次 LLM 调用加多次工具调用，响应时间从秒级变成十秒级甚至分钟级；出了问题要排查是 LLM 理解错了、工具执行错了、记忆漏了还是规划乱了；每次规划、每次工具调用、每次记忆检索都可能产生 LLM 调用费用；LLM 的行为不可预测，同样的输入这次调了正确的工具，下次可能漏调。

以下情况大概率是过度设计：任务就是"用户问一个问题 → LLM 回答"但搞了一套 Agent 框架；只有 2 个工具且调用顺序固定但搞了一个 Planner；对话很短没有跨轮次的上下文需求但搞了一个向量检索的记忆系统；Agent 代码量（框架 + 组件 + 工具注册）比业务逻辑多了 5 倍以上。最经典的信号是：花了一个月搭建 Agent 框架，但实际跑的任务用一个 prompt 就能解决。

更简单的替代方案其实很多。只需要工具调用的话，OpenAI Function Calling / Claude Tool Use 就够了，SDK 原生支持；只需要记忆，把 messages 列表传回去就行；只需要固定流程，写死步骤顺序比 LLM 规划简单得多。Spring 或 Python 项目里，Service + Strategy 模式通常比 Agent 架构更轻量。

```python
# 最简替代方案：不需要完整框架，只需要工具调用
import openai

client = openai.OpenAI(api_key="sk-...")

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取某个城市的天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "北京今天天气怎么样？"}],
    tools=tools,
)

if response.choices[0].message.tool_calls:
    for tool_call in response.choices[0].message.tool_calls:
        args = json.loads(tool_call.function.arguments)
        result = get_weather(**args)
```

注意这里用 `json.loads` 而不是 `eval`。`eval` 会执行任意代码，如果 LLM 返回的参数被恶意注入，后果严重。这个方案的好处是不需要自建 Agent 框架，OpenAI/Anthropic 的 SDK 已经帮你处理了"模型决定调工具 → 你执行 → 结果返回模型"的循环。

## 与其他设计模式的关系

Agent 架构和经典设计模式有不少重叠。ToolRegistry 本质上就是简单工厂加策略模式——每个工具是一个策略，注册表是工厂。Planner 可以在运行时切换不同的规划策略（LLM 规划、模板规划、用户指定），这是策略模式的典型应用。Agent 的主循环（规划 → 执行 → 输出）是一个模板方法，不同 Agent 的区别在于每一步的具体实现。对话过程可以用状态模式管理（初始 → 规划中 → 执行中 → 完成 → 失败），每一步执行都可以触发观察者回调用于日志、监控、用户通知。

## 框架中的实际应用

| 框架 | Agent 架构 | 说明 |
|------|-----------|------|
| **LangChain** | `AgentExecutor` = LLM + Tools + Memory | 最经典的 Agent 框架，四大组件齐全。但代码量大，学习曲线陡 |
| **OpenAI Assistants API** | 内置 Agent 能力 | SDK 层面封装好了工具调用和对话管理，不需要自己搭框架 |
| **CrewAI** | 多 Agent 协作 | 在单 Agent 之上加了"团队"概念，多个 Agent 各司其职 |
| **Anthropic Tool Use** | Claude 原生工具调用 | 不需要第三方框架，Anthropic SDK 直接支持工具调用 |
| **LlamaIndex** | Agent + 检索 | 核心能力是检索增强，但内置了 Agent 执行器 |

## 从"一个 prompt"到 Agent 架构

很多教程直接给完整的 Agent 代码，但不说怎么从简单代码一步步演进。笔者自己走过的路径是这样的：

最开始一个 prompt 搞定——`response = llm.generate("分析这份销售数据，告诉我 Q4 表现")`。然后发现读文件这件事 LLM 做不到，需要工具，于是手动调 `pd.read_csv("sales.csv")` 把结果塞给 LLM。工具从 1 个变成 10 个之后，if-else 判断用哪个工具已经没法看了，于是抽象出 ToolRegistry 来管理。再然后用户说"我喜欢柱状图"，下一轮问"换种图表"的时候 Agent 应该知道换成柱状图——这就必须加记忆了。任务变成"分析数据并生成报告"这种需要多步的事情，一步到位跑不通，于是加了 Planner 来拆解步骤。最后把这些组件组装到一起，就是 Agent 循环了。

这个过程中一个重要的体会是：不需要一开始就设计 Agent 架构。大多数好的架构是从需求里长出来的。先写最简单的代码，发现 LLM 不够用就加工具，发现记不住就加记忆，发现乱来就加规划。一步一步加，每一步都有明确的需求驱动。至少我目前的情况是这样。

## 使用判断标准

该用 Agent 架构的场景（至少命中两条）：

- 任务需要调用外部工具（API、数据库、文件、代码执行）
- 任务需要多轮对话，且跨轮次有上下文依赖
- 任务需要多步操作，且步骤之间有依赖关系
- 需要追踪每一步的执行结果

不该用的场景（命中任意一条就不需要）：

- 任务就是纯文本处理（问答、翻译、总结）
- 不需要外部数据或工具
- 任务只有一两步，步骤固定
- 对响应延迟要求高（< 3 秒）

## 写在最后

Agent 不是一个新技术，它是设计模式在 LLM 时代的应用。LLM 是大脑，记忆是数据库，工具是 API 接口，规划是策略选择。理解了这一点，就不会被各种 Agent 框架牵着走——自己就能判断哪个框架值得用、哪个不值得。

下一篇会深入 Agent 架构中最核心的执行模式——ReAct（思考-行动循环）。这是几乎所有 Agent 框架的基础执行机制，也是最容易被滥用的模式。
