> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 10 · 会记住、会干活的助手：Memory、Tools 与 Agent

> Memory 会话隔离 + @tool 工具说明书 + create_agent 自动循环：会记住、会干活的助手。

# 10 · 会记住、会干活的助手：Memory、Tools 与 Agent

## 开头的现象

老板试用小林的 RAG 问答机，问了一句："我上个月问过你年假的事，你还记得吗？" 问答机回答："我们好像是第一次对话。"

老板皱起眉头。小林在旁边擦了擦汗，心里明白：**RAG 是无状态的，每次提问都是"翻手册"，它根本不记得"这个用户刚才问过什么"。**

更麻烦的还在后面。老板又说："小林，让它顺便帮我查一下明天北京天气，再算一下 1024×768 等于多少——它一个都干不了，只会说'我是文本助手'。"

小林看着那个只会"照着念手册"的问答机，心里冒出一个念头：**"它缺两样东西——'记住'和'干活'。而这两样，正好是第 4 篇的记忆和第 1 篇的模型都解决不了的。我得找新工具。"**

## 第一幕：记忆的"正经做法"——手写历史 + 会话隔离

小林第 4 篇学会了手写历史：`history.append(resp)` 滚雪球。但在 RAG 问答机上，这个雪球有个问题——**每次检索、每次新会话，历史都得重新拼**。

他想要的是：**把"存历史"这件事抽出来，按用户（session\_id）自动管理，每个用户各记各的。**

**纯 langchain\_core 就能做到**——用 `InMemoryChatMessageHistory` 当"存储抽屉"：

```python theme={null}
from langchain_core.chat_history import InMemoryChatMessageHistory

# 记忆管理器：按 session_id（会话ID）存历史
store = {}
def get_history(session_id: str):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()   # 每个会话一个"抽屉"
    return store[session_id]

# 用法：调模型前，先取出该会话的历史，拼进消息列表
history = get_history("user1")          # 取出 user1 的抽屉
history.add_user_message("我叫小林")     # 存用户话
ai_msg = model.invoke(history.messages) # 调模型（历史自动带上）
history.add_message(ai_msg)             # 存 AI 回复

# 另一个用户：user2 的抽屉是空的，互不干扰
history2 = get_history("user2")
print(history2.messages)   # [] —— user2 完全不知道 user1 说过啥
```

小林盯着这个 `session_id` 的概念，脑子里亮了一下：**"这就像浏览器的多标签页——每个标签页（session\_id）有自己的浏览记录，互不串门。用户 A 的对话历史，不会污染用户 B 的。"**

他记下了一个判断：**记忆的本质 = 历史消息 + 会话隔离。** 第 4 篇的手写历史是"手动管理"，`InMemoryChatMessageHistory` 是"按会话分抽屉存"，LangGraph 的 checkpointer 是"生产级：自动存 + 持久化到磁盘"（第 11 篇的事）。**前两个用 langchain\_core 就能做，第三个要 LangGraph。**

> 想验证"会话隔离"吗？跑上面这段代码——`get_history("user1")` 和 `get_history("user2")` 各是各的抽屉。给 user1 存三句话，user2 的 `messages` 还是空的。

**⚠️ 小林的提醒**：你可能在网上看到 `RunnableWithMessageHistory`——那是**官方已废弃**的组件（1.3.3 标记 deprecated，2.0 移除）。**别学它**，直接用上面的手写 + InMemory 方案，或者直接上 LangGraph 的 checkpointer。

## 第二幕：工具——让模型"有手"

记忆搞定了"记住"，接下来是"干活"。小林想要模型能查天气、能算数。他一开始天真地想：**"给模型写 if-else？"** 他写了几行就放弃了——模型可能问一百种方式，你不可能穷举。

他翻到了 `@tool` 装饰器，文档说：**"把普通 Python 函数变成'模型能看懂的工具说明书'。"**

```python theme={null}
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气。"""          # ← 这个 docstring 就是给模型看的"说明书"
    return f"{city}今天晴，28 度。"

@tool
def calculate(expression: str) -> str:
    """计算一个数学表达式，例如 '3 * 4 \+ 5'。"""
    return str(eval(expression))   # 教学演示用，生产别用 eval

# 把工具"绑定"到模型：模型从此知道有这两个工具
bound = model.bind_tools([get_weather, calculate])
```

他打印了 `get_weather` 这个工具长什么样，发现 `@tool` 干了一件神奇的事：**它把函数的名字、docstring、参数类型（`city: str`）翻译成了一段 JSON Schema——一段"工具说明书"，模型读得懂。**

```json theme={null}
{
  "name": "get_weather",
  "description": "查询指定城市的天气。",
  "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] }
}
```

然后他调模型，看到了一段让他起鸡皮疙瘩的输出——**模型第一轮返回的不是回答，是"工具调用申请单"：**

```python theme={null}
resp = model.invoke("北京天气怎么样？顺便算一下 3*4\+5")
for tc in resp.tool_calls:
    print(f"→ 想调用 {tc['name']}({tc['args']})")
# → 想调用 get_weather({'city': '北京'})
# → 想调用 calculate({'expression': '3*4\+5'})
```

小林愣住了：**"模型没回答，它在'申请'——它说'我要调 get\_weather 查北京天气'。它不会真的执行，它在等我的代码去执行。"** 他这才明白工具调用的本质：**模型是"指挥官"，你是"执行者"。模型决定"调哪个、传什么参"，你的代码真的去跑函数，把结果回传。**

> 想验证"申请单"吗？跑 `10_tools.py`——第一轮模型返回带 `tool_calls` 的 AIMessage（content 是 tool\_use 块），你执行工具后把结果作为 `role: "tool"` 消息回传，第二轮模型给出最终回答。三步：模型申请 → 你执行 → 结果回传。

## 第三幕：Agent——把"三步舞"自动化

小林现在会手动做工具循环了，但他很快发现：**如果模型想连调 3 个工具，他要手写 3 轮循环；如果模型想调了工具再调工具，他得写 while 循环。这太累了。**

他查到了 LangChain 的官方定义，差点没笑出声：

> **Agent = Model + Harness（模型 + 鞍具）。模型负责思考，harness 负责工具循环、消息管理、记忆。**

而 `create_agent` 就是那个"鞍具"——你只管给它模型和工具：

```python theme={null}
from langchain.agents import create_agent

agent = create_agent(
    model=model,
    tools=[get_weather, get_time_now, calculate],   # 三种工具
    system_prompt="你是小林公司的智能助手，能用工具回答天气、时间、计算问题。回答简洁自然。",
)
```

然后他看到了 create\_agent 最令人震撼的地方——**它自己会循环调工具，直到给出最终回答：**

```python theme={null}
result = agent.invoke({"messages": [{"role": "user", "content": "上海天气怎么样？现在几点了？"}]})
print(result["messages"][-1].content)
# → "上海今天晴，28度。现在是下午3点。"   ← 自动连调了两个工具，整合成一句回答
```

小林打印了 `result["messages"]`，发现这是一个完整的"流水账"：

```text theme={null}
[human]  上海天气怎么样？现在几点了？
[ai]     tool_calls=[get_weather, get_time_now]   ← 模型第一轮：递申请单
[tool]   上海今天晴，28 度。                      ← 你执行了 get_weather
[tool]   现在是下午 3 点。                        ← 你执行了 get_time_now
[ai]     上海今天晴，28度。现在是下午3点。         ← 模型第二轮：整合回答
```

他恍然大悟：**`result["messages"]` 本身就是历史！** 把它原样传回下一轮，记忆和工具就同时有了：

```python theme={null}
history = list(result["messages"])
history.append({"role": "user", "content": "我刚问的是哪个城市的天气？"})
result2 = agent.invoke({"messages": history})
print(result2["messages"][-1].content)
# → "上海。"   ← 记得！因为历史被传回去了
```

> 想验证"自动循环 + 记忆"吗？跑 `10_agent.py`——三问收官：① 复合问题（自动连调两个工具）② 记忆测试（"我刚问的哪个城市"，它记得）③ 数学计算（自动调 calculate 工具）。

### 🔧 技术细节：这一章涉及的关键类和签名

**① `@tool` 装饰器——把函数变成"模型能读的工具说明书"（langchain\_core.tools）**

```python theme={null}
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气。"""    # docstring = 给模型的说明书
    return f"{city}今天晴，28 度。"

# 生成的 BaseTool 对象：
tool_obj = get_weather
tool_obj.name          # "get_weather"
tool_obj.description   # docstring
tool_obj.args          # {"city": "str"}  ← 从类型注解自动推断的 JSON Schema
tool_obj.invoke({"city": "北京"})   # 手动调用
# 装饰器参数：@tool(args_schema=..., return_direct=..., parse_docstring=True)
```

**② `BaseTool`——工具基类的关键方法**

```python theme={null}
tool.invoke(input: str | dict | ToolCall, config=None) -> Any
tool.run(...)                 # 旧式调用
tool.get_input_schema()       # 返回入参 JSON Schema
tool.tool_call_schema         # ToolCall 结构
```

**③ `create_agent`——把"三步舞"自动化的鞍具（langchain.agents）**

```python theme={null}
create_agent(
    model: str | BaseChatModel,       # 大脑
    tools: Sequence[BaseTool | Callable] | None = None,  # 手
    system_prompt: str | SystemMessage | None = None,    # 人设
    middleware=(),                    # 中间件（进阶）
    response_format=None,             # 结构化输出
    checkpointer=None,                # 持久化记忆（第 11 篇）
    store=None,                       # 跨会话存储
    interrupt_before=None,            # 在哪些节点前暂停（人审）
    interrupt_after=None,
    debug=False,
) -> CompiledStateGraph              # ⚠️ 返回的是 LangGraph 图！
```

**④ agent 返回结构——`result["messages"]` 就是历史**

```python theme={null}
result = agent.invoke({"messages": [{"role": "user", "content": "上海天气？"}]})
result["messages"]        # list[BaseMessage]：完整流水账
result["messages"][-1]    # 最终回答（AIMessage）
result["messages"][-1].content  # 文本
```

> 想验证吗？`get_weather.args` 是一个 dict（自动从类型注解推断）；`print(type(agent))` 显示 `CompiledStateGraph`。

## 第四幕：边界——Agent 不是银弹

小林用了一天，摸清了 Agent 的边界：

**它能干的**：自主决定调哪个工具、调几次、什么时候收手；带记忆（messages 回传）；整合多个工具结果。

**它不能干的 / 代价**：

* **模型决定一切，你控制不了过程**——它可能不调工具直接答（你没法强制它"必须调"），也可能调了没用的工具。
* **循环可能失控**——如果工具一直返回"还有事要做"，模型会一直调下去（有次数上限保护，但成本会涨）。
* **工具越多越容易"选择困难"**——工具描述写得模糊，模型就乱调。工具说明书（docstring）质量直接决定 agent 智商。
* **不是所有任务都该用 Agent**——固定流程（检索→生成）用 Chain（第 9 篇的 RAG）就够了，Agent 是"流程会变"才用。

> 想验证边界吗？给模型绑一个工具但不说明"什么时候该用"，问一个根本不需要工具的问题——模型可能完全不调工具直接答（它觉得没必要）。这就是"模型自主"的代价。

## 结论（小林用一天换来的）

**记忆 = 历史消息 + 会话隔离（session\_id）；工具 = `@tool` 把函数变成"说明书"，模型只负责"申请"（tool\_calls），你负责执行；Agent = `create_agent` 把这套循环自动化——模型思考，harness 干活，`result["messages"]` 就是历史，传回去就有记忆。** 边界：Agent 让模型自主决策，但"自主"意味着"不可控"；固定流程别用 Agent。\*\* 小林现在有了"会记住、会干活的助手"——但他隐隐觉得，`create_agent` 这个黑盒里好像还藏着什么结构……

## 复现信号：什么时候你会想起这一章

1. **想让模型"记得用户"**——历史 + session\_id 隔离；或直接用 Agent（messages 本身就是历史）。
2. **想让模型"干活"（查库、算数、查天气）**——`@tool` 定义工具 + `bind_tools`，模型返回 tool\_calls 你执行。
3. **想让模型"自主决定调哪些工具、调几次"**——`create_agent(model, tools, system_prompt)`。
4. **报错"tool\_calls 为空"或"模型不调工具"**——不是代码错，是模型自主决定；检查工具说明书写清楚没有。

小林的助手能记住、能干活了。但小林总觉得 create\_agent 是个黑盒——他不知道模型"下一步干嘛"、"循环到哪了"、中途能不能插手。老板还问："这个机器人能不能在我确认之后再执行操作？" 小林知道，他需要真正的"图"了——[翻到第 11 篇：从流水线到会转弯的图](/doc/doc/narrative-course/11-LangGraph)。
