《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本质:存储+提取=上下文注入

核心原理
  1. Save:保存每轮 用户输入 + AI 回复 到存储介质(内存/数据库)

  2. Load:新对话时,自动提取历史并拼接到 Prompt 中

  3. 效果:避免重复提问、支持复杂任务、简化交互

    💡 类比理解:类似你和朋友聊天,对方知道你的名字,而不是人机般每次聊天都要问一句“你是谁?”

优势在于支持会话隔离(基于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条信息),到第六轮时,第一轮的信息已经被截断

⚠️:身份信息、偏好等关键信息不能够依赖普通窗口记忆

我认为可以:

  1. 关键的信息进行单独存储,放到数据库或者存入session中,不走对话历史
  2. 混合记忆,窗口记忆+关键实体缓存(Redis中缓存关键实体信息)
  3. Prompt:加上对关键信息的约束和要求:“必须保留用户姓名、职业等身份信息”
核心代码差异

三种链路基本一致,但是历史管理逻辑不同

  1. 全量记忆

    无需特殊处理,InMemoryChatMessageHistory自动追加所有信息

  2. 窗口记忆(手动截断)

    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
    
  3. 摘要记忆(动态生成摘要)

    # 在 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)

实践思考
  1. 要警惕任意代码的执行

    在数学计算案例中使用了PythonREPLTool,它能执行任意 Python 代码,这会有一定风险

    所以在案例中我设置了:

    • 只允许数字、运算符能够输入

    • 沙箱环境,隔离在生产环境外

    • 权限最小化,只有必要的文件/网络权限

  2. 参数校验:用 Pydantic 强约束

    在温度转换工具中,我定义了 TemperatureConvertInput 模型:

    class TemperatureConvertInput(BaseModel):
        temperature: float = Field(description="需要转换的温度值")
        from_unit: str = Field(description="只能是 celsius 或 fahrenheit")
        
    
  3. 使用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的管道符串成流水线,后续要加新的功能时只需加新的模块而不用大改

三、总结&下一步

本章学习了:

  1. Memory组件:解决LLM“健忘”的问题,重点掌握三种常用的记忆组件(全量、窗口、摘要)的适用场景和方法
  2. Tool组件:让LLM可以调用外部工具,明白内置工具的使用和自定义工具的规范
  3. 组件组合:通过LCEL实现模块流水线,重点掌握“记忆+工具+Agent”的组合思路,能构建带记忆、能行动的复杂应用

后面通过Agent的高级用法去升级智能体的能力

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐