《LangChain 实战:用Memory和Tool打造有记忆、能行动的智能体》
《LangChain 实战:用Memory和Tool打造有记忆、能行动的智能体》
前言
本文基于 DataWhale 的 Easy-LangAgent 教程 实践总结,聚焦 LangChain 两大进阶组件:Memory(状态管理) 与 Tool(外部工具调用)。
通过亲手实现三种记忆模式 + 自定义工具 + 组合智能体,我将带你构建一个既能记住你是“小明”,又能帮你算
√668的 AI 助手。代码已开源:[AI-Agent-learning](bingege-0729/AI-Agent-learning: 个人通过DataWhale对智能体学习的总结)
一、为什么需要进阶组件
在前两章中我学习了如何从0-1启动智能体、如何写一个合规的提示词以及如何简化提示词工程撰写、控制AI输出的格式。但是如果要升级成一个真正的智能体,还缺乏状态的管理和工具的调用的能力
这导致很多时候处理完成再回头时记不住上文
而LangChain中的Memory组件和Tool能够很好的解决这些问题
二、组件
拥有“记忆”:Memory组件实战
大语言模型(LLM)本身是无状态的——每次请求都是独立的,无法主动记住你上一句说了什么。
LangChain 的 Memory 组件通过“存储 + 提取”机制,将历史对话注入 Prompt,从而实现多轮连贯对话。
Memory本质:存储+提取=上下文注入
核心原理
-
Save:保存每轮
用户输入+AI 回复到存储介质(内存/数据库) -
Load:新对话时,自动提取历史并拼接到 Prompt 中
-
效果:避免重复提问、支持复杂任务、简化交互
💡 类比理解:类似你和朋友聊天,对方知道你的名字,而不是人机般每次聊天都要问一句“你是谁?”
优势在于支持会话隔离(基于session_id)、自动管理历史信息的注入与保存、搭配现代LLM模型的会话交互逻辑
LangChain中推荐使用LCEL架构(RunnableWithMessageHistory
+BaseChatMessageHistory)抽象类实现对话记忆管理,通过管道符|` 串联组件,实现清晰的流水线:base_chain = full_memory_prompt | llm # 类似👆这种个人觉得跟我们后端Java中的lambda表达式有点相似
三种记忆模式的对比
LangChain 提供多种 Memory 实现,最常用的是以下三种:
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全量记忆 | 上下文完整 | Token 爆炸 | 短对话、咨询 |
| 窗口记忆 | 控制成本 | 可能丢关键信息 | 中长对话、客服 |
| 摘要记忆 | 超长对话支持 | 丢失细节、额外开销 | 长期陪伴、复杂任务 |
关键区别:如何管理历史消息的长度与内容
具体代码实现可以移步DataWhale 的 Easy-LangAgent 教程学习
思考
在实测窗口记忆时,我模拟了6轮对话:
1. 我叫小红
2. 我喜欢画画
3. 我来自上海
4. 我是一名学生
5. 我刚才说我来自哪里? → ✅ 正确回答“上海”
6. 我叫什么名字? → ❌ 回答“我不知道”
原因:窗口大小我设置成2(也就是保留最近2轮,4条信息),到第六轮时,第一轮的信息已经被截断
⚠️:身份信息、偏好等关键信息不能够依赖普通窗口记忆
我认为可以:
- 关键的信息进行单独存储,放到数据库或者存入session中,不走对话历史
- 混合记忆,窗口记忆+关键实体缓存(Redis中缓存关键实体信息)
- Prompt:加上对关键信息的约束和要求:“必须保留用户姓名、职业等身份信息”
核心代码差异
三种链路基本一致,但是历史管理逻辑不同:
-
全量记忆
无需特殊处理,
InMemoryChatMessageHistory自动追加所有信息 -
窗口记忆(手动截断)
def get_window_memory_history(session_id: str): history = get_or_create_history(session_id) # 👇 核心:只保留最近 WINDOW_SIZE 轮(每轮2条消息) if len(history.messages) > 2 * WINDOW_SIZE: history.messages = history.messages[-2 * WINDOW_SIZE:] return history -
摘要记忆(动态生成摘要)
# 在 LCEL 链中插入摘要生成步骤 summary_base_chain = ( RunnablePassthrough.assign( chat_summary=lambda x: summary_chain.invoke({ "chat_history_text": "\n".join(f"{m.type}: {m.content}" for m in x["chat_history"]) }).content ) | prompt_with_summary # 注入 {chat_summary} | llm )调用摘要也是会额外调用LLM,生产环境中可以调整为5-7轮一次更新
“组件”-Tool讲解和实战
Tool的存在是为了弥补LLM无法读文件、做精确计算的缺点
核心流程:思考👉决策👉调用👉整合结果
核心组件
- Tool:具体的干活工具
- Toolkit:把相关的工具到抱在一起
- Agent:只会协调LLM和工具是否要调用、怎么用
案例:查询天气
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os
# ======================
# 1. 环境(这里省略)
# ======================
# ======================
# 2. 工具
# ======================
@tool
def weather_query(city: str) -> str:
"""查询指定城市天气"""
weather_data = {
"北京": "北京今日天气:晴,-2~8℃",
"上海": "上海今日天气:多云,5~12℃",
"广州": "广州今日天气:小雨,18~25℃",
}
return weather_data.get(city, f"暂无 {city} 数据")
tools = [weather_query]
# ======================
# 3. 创建 Agent(开启 debug)
# ======================
agent = create_agent(
model=llm,
tools=tools,
debug=True, # 👈 打开过程打印
)
# ======================
# 4. 运行
# ======================
response = agent.invoke({
"messages": [
{"role": "user", "content": "北京今天的天气怎么样?"}
]
})
print("\n最终回答:")
print(response["messages"][-1].content)
案例总AI的思考流程是 WeatherQuery 工具 → 传入参数 “北京” → 工具返回结果 → AI 整理成自然语言回答。
其实也就是人做一件事的方式:思考->选择工具、使用工具->总结反馈
自定义工具(@tool装饰器)
前面的案例中发现LangChain提供了装饰器,这样可以快速创建自己的工具
自定义Tool的三要素
LangChain通过@tool装饰器简化工具创建,但让AI正确、安全地使用工具,需要:
| 要素 | 作用 | 示例 |
|---|---|---|
| 清晰的 docstring | 告诉 AI 工具用途和参数 | """查询指定城市天气""" |
| 明确的参数类型 | 用 Pydantic 或类型注解约束输入 | city: str |
| 安全的返回值 | 避免暴露内部错误,返回用户友好结果 | "暂无 {city} 数据" 而非抛异常 |
⚠️ 关键提醒:AI 完全依赖
docstring判断是否调用工具!描述模糊会导致误用或不用。
案例:温度转换
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import os
# ======================
# 1. 环境变量(此处省略)
# ======================
# ======================
# 2. 参数模型
# ======================
class TemperatureConvertInput(BaseModel):
temperature: float = Field(description="需要转换的温度值,例如37.0")
from_unit: str = Field(description="原始温度单位,只能是celsius或fahrenheit")
# ======================
# 3. 工具
# ======================
@tool(args_schema=TemperatureConvertInput)
def temperature_converter(temperature: float, from_unit: str) -> str:
"""温度单位转换工具"""
if from_unit not in ["celsius", "fahrenheit"]:
return f"错误:单位'{from_unit}'不合法"
if from_unit == "celsius":
fahrenheit = temperature * 9/5 + 32
return f"{temperature}摄氏度 = {fahrenheit:.2f}华氏度"
else:
celsius = (temperature - 32) * 5/9
return f"{temperature}华氏度 = {celsius:.2f}摄氏度"
tools = [temperature_converter]
system_prompt = """
你是一名专业温度转换助手,只能使用temperature_converter工具完成计算。
"""
# ======================
# 4. 创建 Agent
# ======================
agent = create_agent(
model=llm,
tools=tools,
system_prompt=system_prompt,
debug=True
)
# ======================
# 5. 运行
# ======================
if __name__ == "__main__":
query = "将37摄氏度转换为华氏度"
response = agent.invoke({
"messages": [{"role": "user", "content": query}]
})
print("\n最终结果:")
print(response["messages"][-1].content)
实践思考
-
要警惕任意代码的执行
在数学计算案例中使用了
PythonREPLTool,它能执行任意 Python 代码,这会有一定风险所以在案例中我设置了:
-
只允许数字、运算符能够输入
-
沙箱环境,隔离在生产环境外
-
权限最小化,只有必要的文件/网络权限
-
-
参数校验:用 Pydantic 强约束
在温度转换工具中,我定义了
TemperatureConvertInput模型:class TemperatureConvertInput(BaseModel): temperature: float = Field(description="需要转换的温度值") from_unit: str = Field(description="只能是 celsius 或 fahrenheit") -
使用LangChain提供的丰富工具包
小结:Tool不是功能,而是把智能体的能力进行扩展
综合实践:Memory+Tool=真正智能体
学习了组件以后,重要的是如何进行组合
前置配置
需要额外安装langchain-experimental包
pip install langchain-experimental
带记忆的对话机器人
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_openai import ChatOpenAI # 补充LLM定义
from langchain_experimental.tools import PythonREPLTool # 数学计算工具
from langchain_core.messages import HumanMessage, AIMessage
from dotenv import load_dotenv
import re
import os
# 前置配置这里这里省略,复制代码时需要补充
# 初始化数学计算工具(PythonREPL)
calc_tool = PythonREPLTool()
# 窗口记忆大小:保留最近2轮对话(每轮=用户+助手消息)
WINDOW_SIZE = 2
# -------------------------- 2. 定义提示词模板(适配窗口记忆+工具调用) --------------------------
prompt = ChatPromptTemplate.from_messages([
("system", """你是一名友好的个人助手,规则如下:
1. 能记住最近{window_size}轮对话内容,用简单语言解答问题;
2. 如果问题包含数学计算(如加减乘除、公式、数值运算),先调用计算工具得到结果,再用自然语言解释;
3. 非计算问题直接回答,记得结合历史对话上下文。"""),
MessagesPlaceholder(variable_name="chat_history"), # 窗口记忆注入点
("human", "{input}") # 用户新问题
])
# -------------------------- 3. 工具调用逻辑(判断是否需要计算) --------------------------
def judge_and_calc(inputs):
"""
核心逻辑:
1. 检测用户问题是否包含数学计算需求
2. 是:调用PythonREPLTool计算,再结合LLM生成回答
3. 否:直接用LLM回答
"""
user_input = inputs["input"]
chat_history = inputs["chat_history"]
# 简单的计算意图检测(可根据需求扩展)
calc_pattern = r"(\+|\-|\×|\*|÷|/|=|计算|求和|求差|平方|立方|多少|等于)"
is_calc_needed = bool(re.search(calc_pattern, user_input))
if is_calc_needed:
# 步骤1:调用计算工具执行运算
try:
# 提取计算表达式(简化版:取数字和运算符部分)
calc_expr = re.sub(r"[^\d\+\-\*\/\(\)\.]", "", user_input)
if not calc_expr:
calc_result = "未识别到可计算的表达式"
else:
calc_result = calc_tool.run(calc_expr)
except Exception as e:
calc_result = f"计算出错:{str(e)}"
# 步骤2:构造包含计算结果的提示,让LLM生成自然语言回答
enhanced_input = f"""
用户问题:{user_input}
计算过程/结果:{calc_result}
请结合计算结果,用简单易懂的语言回答用户问题,同时参考历史对话:{chat_history}
"""
inputs["input"] = enhanced_input
return inputs
# -------------------------- 4. 窗口记忆实现(仅保留最近N轮) --------------------------
# 会话存储:key=session_id,value=InMemoryChatMessageHistory
window_memory_store = {}
def get_window_session_history(session_id: str) -> BaseChatMessageHistory:
"""获取带窗口限制的会话历史,自动截断超出长度的消息"""
# 初始化会话记忆(无则创建)
if session_id not in window_memory_store:
window_memory_store[session_id] = InMemoryChatMessageHistory()
history = window_memory_store[session_id]
# 截断逻辑:保留最近WINDOW_SIZE轮(每轮2条消息:Human+AI)
total_messages = len(history.messages)
if total_messages > 2 * WINDOW_SIZE:
history.messages = history.messages[-2 * WINDOW_SIZE:] # 只保留最后N轮
return history
# -------------------------- 5. 构建完整的LCEL链(记忆+工具+LLM) --------------------------
# 核心链:参数传递 → 计算判断 → 提示词拼接 → LLM生成
chain = (
RunnableLambda(judge_and_calc)
| prompt
| llm
)
# 注入窗口记忆功能
chain_with_window_memory = RunnableWithMessageHistory(
runnable=chain,
get_session_history=get_window_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="output"
)
# -------------------------- 6. 多轮对话测试 --------------------------
if __name__ == "__main__":
session_id = "student_001" # 每个用户独立会话ID,记忆隔离
print("===== 带窗口记忆的数学计算智能助手 =====")
print("支持:多轮对话、仅保留最近2轮记忆、自动数学计算")
print("输入'退出'结束对话\n")
while True:
user_input = input("你:")
if user_input in ["退出", "quit", "q"]:
print("助手:再见!有问题随时问我~")
break
# 调用带窗口记忆的智能体
response = chain_with_window_memory.invoke(
{"input": user_input, "window_size": WINDOW_SIZE},
config={"configurable": {"session_id": session_id}}
)
# 输出回答(并将对话存入记忆)
print(f"助手:{response.content}\n")
【LCEL】使得逻辑更加清晰,后续增加工具,只需在链路上再加一个工具组件,不用重构整个代码
但是PythonREPLTool 可执行任意代码,生产环境需限制可执行的表达式(如仅允许加减乘除)。
总结
从案例中可以看出核心思路是“模块化拆分+流水线组合”:把应用拆分成独立的“小模块”,然后用LCEL的管道符串成流水线,后续要加新的功能时只需加新的模块而不用大改
三、总结&下一步
本章学习了:
- Memory组件:解决LLM“健忘”的问题,重点掌握三种常用的记忆组件(全量、窗口、摘要)的适用场景和方法
- Tool组件:让LLM可以调用外部工具,明白内置工具的使用和自定义工具的规范
- 组件组合:通过LCEL实现模块流水线,重点掌握“记忆+工具+Agent”的组合思路,能构建带记忆、能行动的复杂应用
后面通过Agent的高级用法去升级智能体的能力
更多推荐

所有评论(0)