# LangChain Agent 实战笔记：旅游搭子 AI 助手搭建

> 记录时间：2026-07-02
> 项目：旅游搭子 AI 助手（LangChain 重构版）
> 关联：L3 实战项目

---

## 一、项目架构概览

```
旅游搭子AI助手/
├── main.py          # 主入口：Agent 创建 + 对话循环
├── memory.py        # 记忆系统：用户画像 + 旅行记忆 JSON 持久化
├── Agent人设.md      # Agent 角色设定（给 LLM 的系统提示参考）
├── README.md
├── requirements.txt
└── .env             # API Key（OPENAI_API_KEY + OPENAI_BASE_URL）
```

核心链路：
```
用户输入 → chat() → AgentExecutor.invoke()
  → ChatPromptTemplate 组装（system + chat_history + input + agent_scratchpad）
  → LLM 推理 → 决定是否调用工具 → 工具写入 JSON 文件
  → 提取 output → 追加历史 → 返回
```

---

## 二、LangChain Agent 创建（核心知识点）

### 2.1 两个关键组件

```python
from langchain.agents import create_tool_calling_agent, AgentExecutor

# 第一步：创建 Agent（定义"怎么想"）
agent = create_tool_calling_agent(
    model=llm,       # ChatOpenAI 实例
    tools=tools,     # 工具列表
    prompt=prompt,   # ChatPromptTemplate
)

# 第二步：创建 Executor（定义"怎么做"——执行循环）
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,    # 打印中间推理过程
)
```

**关键理解**：`create_tool_calling_agent` 只定义 Agent 的"大脑"（如何根据 prompt 和工具决定下一步），`AgentExecutor` 是"手脚"（实际执行调用循环、处理 tool_calls、管理 agent_scratchpad）。

### 2.2 ⚠️ 踩坑与认知迭代：create_agent 的"消失"与"回归"

**第一轮认知（错误）**：LangChain >=0.2 废弃了 `create_agent`，必须用 `create_tool_calling_agent` + `AgentExecutor`。

**第二轮认知（2026-07-02 验证）**：LangChain **1.0**（2025年10月发布）重新引入了 `create_agent` 作为全新统一 API，底层基于 LangGraph runtime。1.3.x 实测存在且可用。

```python
# LangChain 1.3.x 的正确用法
from langchain.agents import create_agent

agent = create_agent(
    model=llm,      # 支持 "provider:model" 字符串格式
    tools=tools,    # 工具列表
    # system_prompt="..."  ← 可选，但注意是静态的！
)
```

**两代 create_agent 的本质区别**：

| | 旧版（<0.2） | 新版（1.0+） |
|---|---|---|
| 底层 | AgentExecutor | LangGraph StateGraph |
| 返回值 | Runnable | CompiledStateGraph |
| system_prompt | 字符串参数 | 字符串参数（静态，不随每轮变化） |
| 调用方式 | `agent_executor.invoke({"input": ...})` | `agent.invoke({"messages": [...]})` |

### 2.3 动态记忆注入的折中方案

`create_agent` 的 `system_prompt` 参数是静态的——Agent 创建时一次传入，无法每轮动态更新记忆上下文。

**解决方案**：不传 `system_prompt`，改用手动在 messages 列表中注入 `SystemMessage`：

```python
# Agent 创建时不传 system_prompt
agent = create_agent(model=llm, tools=tools)

# 每轮对话手动拼 messages
def chat(user_input: str) -> str:
    messages = [
        SystemMessage(content=build_prompt()),  # 动态生成（含最新记忆）
    ]
    messages.extend(chat_history[-20:])         # 历史
    messages.append(HumanMessage(content=user_input))  # 当前输入
    
    result = agent.invoke({"messages": messages})
    # 从 result["messages"] 提取最后一条 AIMessage
```

**为什么不传 system_prompt + 手动加 SystemMessage？** 会导致 system prompt 注入两遍——Agent 模板自动插一遍，你又手动加一遍。

### 2.4 ChatPromptTemplate 的使用（旧方案参考）

```python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", SYSTEM_PROMPT),                             # 系统提示（含 {user_context} {trip_context} 占位符）
    MessagesPlaceholder(variable_name="chat_history", optional=True),  # 对话历史
    ("human", "{input}"),                                  # 当前用户输入
    MessagesPlaceholder(variable_name="agent_scratchpad"), # Agent 推理草稿纸（自动管理）
])
```

**agent_scratchpad 的作用**：Agent 多步推理时的"草稿纸"。Agent 调用工具 A 后，中间思考结果写入 scratchpad，再决定下一步——你不需要手动管，AgentExecutor 自动填充。

调用时变量通过 `invoke()` 传入：

```python
result = agent_executor.invoke({
    "input": user_input,                    # → {input}
    "chat_history": chat_history[-20:],     # → MessagesPlaceholder("chat_history")
    "user_context": memory.get_user_context(),   # → {user_context}
    "trip_context": memory.get_trip_context(),   # → {trip_context}
})
```

### 2.5 最终方案 vs 两种旧方案对比

| | 方案A：手动拼接 | 方案B：AgentExecutor | ✅ 方案C：create_agent 1.0 |
|---|---|---|---|
| Agent 创建 | `create_agent()`（不存在） | `create_tool_calling_agent` + `AgentExecutor` 两步 | `create_agent(model, tools)` 一步 |
| 底层引擎 | — | AgentExecutor | LangGraph StateGraph |
| 消息组装 | 手动拼 messages | ChatPromptTemplate 自动组装 | 手动拼 messages（动态记忆） |
| 响应提取 | 倒序遍历 messages | `result["output"]` 直接拿 | 倒序遍历 `result["messages"]` |
| system_prompt | 字符串参数 | 模板占位符 `{user_context}` | 不传 → 每轮手动注入 SystemMessage |
| 记忆动态性 | ✅ 每轮 build_prompt() | ✅ 模板变量 | ✅ 每轮 build_prompt() |
| 代码量 | ~20 行 | ~10 行 | ~15 行 |
| 适用场景 | — | 记忆结构固定 | **记忆需要每轮动态刷新** |

---

## 三、工具定义（@tool 装饰器）

```python
from langchain.tools import tool

@tool
def update_user_preference(key: str, value: str) -> str:
    """写入用户偏好"""
    # 实际写入 memory 系统
    return f"已更新偏好：{key} = {value}"
```

LangChain 的 `@tool` 装饰器会自动：
- 从函数签名生成参数 schema（给 LLM 看）
- 从 docstring 生成工具描述（帮助 LLM 决定何时调用）
- 包装成 LangChain Tool 对象

---

## 四、记忆系统设计

`TravelMemory` 类（memory.py）：
- **双文件持久化**：`user_profile.json`（偏好+纠错）、`travel_memory.json`（行程+决策+预算+笔记）
- **上下文注入**：`get_user_context()` / `get_trip_context()` 返回文本，注入 system prompt
- **设计理念**：不是向量检索，而是结构化 JSON + 直接文本注入——适合短会话场景

### 4.1 偏好自动提取（_auto_extract）

之前 memory.py 虽然定义了 `EXTRACTION_PROMPT` 和 `EXTRACTION_DECISION_PROMPT`，但**从未被调用**（问题 #5）。修复方案：

```python
class ChatSession:
    def _auto_extract(self) -> None:
        """会话退出时，用 LLM 回顾对话，提取遗漏的偏好和决策"""
        recent = self.get_recent_conversation()  # 取出本轮对话文本
        prompt = EXTRACTION_DECISION_PROMPT.format(conversation=recent)
        response = llm.invoke(prompt)  # LLM 判断是否有新信息需要提取
        # 如果有，再调 EXTRACTION_PROMPT 提取具体的 key-value
```

**关键设计**：
- **触发时机**：用户输入 `quit` 时自动触发，不需要手动调用
- **两阶段提取**：先判断"有没有值得提取的信息"（省 token），有才做详细提取
- **输出格式**：LLM 返回 JSON，`_parse_json_from_response()` 解析后写入 memory 文件

### 4.2 ChatSession 封装（消除全局变量）

之前 `chat_history` 是模块级全局变量（问题 #7），多用户或多轮测试会互相污染。修复后：

```python
class ChatSession:
    def __init__(self, agent, memory, llm):
        self.agent = agent
        self.memory = memory
        self.llm = llm
        self.chat_history: list = []  # ← 实例级，每次 new ChatSession 隔离开
    
    def chat(self, user_input: str) -> str:
        # 复用 self.chat_history，不再需要 global 声明
        ...
    
    def get_recent_conversation(self) -> str:
        """将 chat_history 转成纯文本，供 _auto_extract 使用"""
```

**好处**：
- 不再需要 `global chat_history`
- 可以同时跑多个独立会话而不互相干扰
- `get_recent_conversation()` 为自动提取提供原始对话文本

---

## 五、Claude Code 协作经验

### 5.1 Claude Code 分析项目时的行为模式
1. 先 `ls` 扫目录结构
2. 挑核心文件精读（main.py / memory.py / CLAUDE.md）
3. 结构化输出（文件树 → 逐行分析 → 数据流 → 问题清单）

### 5.2 Claude Code 的修改模式
- 先改 import（顶部），再改核心逻辑（中间），最后改调用方（底部）
- 一次只改一个关注点，分多次 `Edit file`
- 改完后会总结改动内容

### 5.3 学到什么
- Agent 框架的选择：LangChain Agent 提供标准化的 tool calling 循环，比自己写 while + if 更可靠
- 定义与执行分离：`create_tool_calling_agent`（定义）+ `AgentExecutor`（执行）关注点分离 → LangChain 1.0 用 LangGraph 状态图统一了这两个角色
- Prompt 模板化：不用手拼字符串，`ChatPromptTemplate` + `MessagesPlaceholder` 防注入错误
- **批量修复模式**：Claude Code 会把低风险的关联问题（#2 #6 #8 都是配置/环境类）打包一起修，中等复杂度的问题（#5 #7 涉及代码结构）单独修——不是机械地按编号顺序来
- **自动提取的设计思路**：不是每轮对话都调 LLM 提取（太费 token），而是会话结束时一次性回顾——"懒提取"模式，兼顾功能完整性和成本

### 5.4 模型分层调用：Pro 管对话，Flash 管提取
**双模型分流**（2026-07-02）：
```python
# .env
DEEPSEEK_PRO_MODEL=deepseek-chat       # 主对话 → Pro
DEEPSEEK_FLASH_MODEL=deepseek-v4-flash # 后台提取 → Flash

# main.py
llm_pro = ChatOpenAI(model=os.getenv("DEEPSEEK_PRO_MODEL"), ...)    # Agent 用
llm_flash = ChatOpenAI(model=os.getenv("DEEPSEEK_FLASH_MODEL"), ...) # _auto_extract 用
```

**设计逻辑**：
- **Pro 负责对话**：用户直接感知质量，多花一点 token 换更好的推理和表达
- **Flash 负责提取**：偏好/决策提取是结构化任务，不需要深度推理，Flash 够用且便宜 5-10 倍

**这是"任务分层"思路的首个落地**：同厂商内按任务复杂度分级——和跨厂商路由（DeepSeek Flash → DeepSeek Pro → Gemini → Claude Opus）是同一套哲学的不同粒度。**核心原则不变：什么任务用什么模型，不让简单活烧贵算力。**

### 5.5 跨模型编程路由方案（2026-07 基准数据）
| 层 | 模型 | SWE-bench | 输出价/Mt | 适用场景 |
|---|---|---|---|---|
| 默认层（70%） | DeepSeek V4 Flash | ~50% | ¥2 | 读代码/写注释/简单修复 |
| 推理层（15%） | DeepSeek V4 Pro | ~55% | ¥6 | 跨文件重构/复杂 bug |
| 性价比层（10%） | Gemini 3.5 Flash | ~54% | $9 | 英文文档/多模态 |
| 核武层（5%） | Claude Opus 4.8 | 69.2% | $25 | 代码审查/架构设计 |

**月费估算**（80 次/天）：¥68 vs 全用 Opus 的 ¥600，省 89%。

---

## 六、问题修复进度

| # | 问题 | 严重度 | 状态 |
|---|------|--------|------|
| 1 | create_agent API 不存在 → **1.3.x 实测存在**，最终方案：不传 system_prompt + 手动注入 SystemMessage | 🔴→✅ | 已修复（2026-07-02） |
| 2 | .env 无 Key + 缺 .gitignore | 🟡→✅ | 已修复（2026-07-02）：补齐 .gitignore + .env |
| 3 | System prompt 注入两遍 → 最终方案绕过了这个问题（不传 system_prompt） | 🟡 | 已规避 |
| 4 | search_travel_info 是空壳 | 🟡 | 待修复 |
| 5 | memory.py 偏好提取 prompt 定义了但从未调用 → `_auto_extract()` 在 quit 时自动触发 | 🟡→✅ | 已修复（2026-07-02） |
| 6 | 环境变量命名混乱（OPENAI_* → DEEPSEEK_*） | 🟢→✅ | 已修复（2026-07-02） |
| 7 | chat_history 模块级全局变量 → 封装为 `ChatSession` 类 | 🟢→✅ | 已修复（2026-07-02） |
| 8 | requirements.txt 有未使用的 fastapi / uvicorn | 🟢→✅ | 已修复（2026-07-02） |

### 首跑验证（2026-07-02）
- ✅ 导入正常：`from langchain.agents import create_agent` 成功
- ✅ Agent 创建成功：`create_agent(model=llm, tools=tools)` 
- ✅ 记忆文件加载：`data/user_profile.json` 4 条偏好数据
- ✅ REPL 正常启动和退出
- ⚠️ 实际 LLM 对话尚未测试（需 .env 配置 DeepSeek Key）
