Memory记忆机制

实际上,LLM本身没有真正的记忆,每次调用都像一次全新的对话。如果只发送当前问题而不提供历史记录,模型并不知道之前聊过什么。因此,多轮对话的实现方式并不是让模型记住内容,而是由应用程序保存历史对话,并在每次提问时将历史记录和当前问题一起发送给LLM,使模型看起来像拥有持续记忆一样。

举一个更完整的例子,可以直观说明上下文在对话中的作用。假设我们有一个周末出行规划助手。在第一轮中,用户提出一个模糊需求:

用户:帮我规划一下这个周六的出行。 AI:好的,请问你想去哪里,大概几点出发?

此时,LLM已建立基本语境,即围绕周六出行规划展开,并引导用户补充关键信息。随后,用户提供细节:

用户:去植物园,上午10点左右。

如果这句话单独输入,它只是一个零散片段,由于缺少背景信息,模型难以准确判断其意图,可能将其理解为日程记录或简单描述。

但当上下文一并输入时,语境变得完整,模型可以基于前后关联信息正确理解其含义:

用户:帮我规划一下这个周六的出行。 AI:好的,请问你想去哪里,大概几点出发? 用户:去植物园,上午10点左右。

可以看到,对话之所以能够连贯,并不是因为模型具备记忆能力,而是因为每一次请求都会将必要的上下文一起传入模型,模型只是基于当前输入重新进行理解与生成。

记忆

在LangChain框架中,负责管理对话历史的组件称为Memory,其本质是一个专门保存聊天记录的地方,用于保存每一轮的用户输入与模型输出。例如,可以通过InMemoryChatMessageHistory类实现这一机制。基于这一设计,多轮对话的执行流程如下:

  1. 接收用户输入。
  2. 从Memory中读取历史对话。
  3. 将历史对话与当前问题组合成完整上下文。
  4. 调用模型生成回复。
  5. 将回复返回给用户。
  6. 将本轮对话写入Memory。

因此,多轮对话的关键不在于模型真正记住了过去的内容,而在于应用程序持续保存并提供历史对话。模型每次回答时,实际上都是基于当前问题和历史记录共同进行推理,从而保持对话的连贯性。关于长期记忆机制,可参考:深入解读LangChain 1.0 Agent长期记忆技术要点。

示例代码如下:

from langchain_core.chat_history import InMemoryChatMessageHistory # 创建 Memory history = InMemoryChatMessageHistory() # 添加用户消息 history.add_user_message("您好,我喜欢每天跑步1小时") # 添加回复消息 history.add_ai_message("我喜欢游泳") # 添加用户消息 history.add_user_message("我每天跑步多久?") response = chat_model.invoke(history.messages) print(response.content)

记忆裁剪

在多轮对话里若不加以限制,历史记录会持续膨胀。常见做法是按轮数裁剪,只保留最近K轮对话,更早的内容直接丢弃:

from langchain_classic.memory import ConversationBufferWindowMemory from langchain_classic.chains import ConversationChain from langchain_community.llms.fake import FakeListLLM llm = FakeListLLM( responses=["OK"] * 10 ) memory = ConversationBufferWindowMemory( k=3, return_messages=True ) conversation = ConversationChain( llm=llm, memory=memory ) for i in range(5): conversation.predict( input=f"第{i+1}轮对话" ) print(f"\n=== 第{i+1}轮后 ===") history = memory.load_memory_variables({})["history"] for message in history: print(message.content)

以上代码本质上实现的是一个固定长度的滑动窗口。运行结果如下:

  • 第1~3轮:正常累加
  • 第4轮:第1轮被移除
  • 第5轮:第2轮被移除

3.3 Tools工具

LLM擅长理解意图、推理和生成文本,但很多实际任务仅依赖生成能力并不足够,例如查询天气需要实时数据、查询订单需要访问业务系统,而这些能力通常不在模型内部,因此需要通过Tool机制将外部能力封装为可调用接口。Tool本质上是对某项功能的结构化描述和执行封装,类似函数调用,常见场景包括数学计算、时间获取、天气查询、数据库检索以及业务系统接口等。

一个Tool通常包含名称、描述、参数定义和执行逻辑,其中名称用于标识工具,描述帮助模型判断是否调用,参数定义约束输入格式,执行逻辑负责完成实际操作。在执行过程中,系统首先将工具注册给模型或Agent,当模型判断需要外部能力时,会根据用户输入和工具描述选择合适的工具并生成调用参数,待工具执行完成后再将结果返回给模型,最终由模型生成回答。对于这一机制的实现原理与实践方式,可以参考:How to Build a Custom AI Agent with LangChain and Python: A Step by Step Guide。

使用@tool定义工具

在LangChain中,可通过@tool装饰器将普通Python函数封装为Tool,函数名称、文档字符串和参数类型会自动提取为结构化元信息,用于描述工具能力。

from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """获取指定城市的天气信息""" weather_data = { "A": "晴天,25°C", "B": "多云,22°C", } return weather_data.get(city, f"无法获取{city}天气") # 直接调用工具,不经过 LLM result = get_weather.invoke({"city": "A"}) print(result) # 晴天,25°C # 查看工具元信息 print(f"工具名: {get_weather.name}") print(f"描述: {get_weather.description}") print(f"参数: {get_weather.args}")

通过@tool定义后,该函数既可以作为普通Python函数直接调用,也可以作为工具被模型或Agent使用,两者的区别在于前者不涉及模型参与,而后者会进入工具调用流程,由模型决定是否调用并生成参数。更多介绍可参考:LangChain 实战教程:从入门到实战。

将工具绑定到模型

工具定义完成后,可以通过bind_tools方法将多个工具绑定到模型,使模型在处理输入时能够感知这些工具。当用户输入到来时,模型先判断是否需要调用外部能力,如果需要,则选择合适工具并生成调用参数。整个过程中,模型只负责工具选择与参数生成,工具的实际执行由开发者完成并返回结果。

from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """获取城市天气""" return f"{city}:晴天 25°C" @tool def search_database(query: str) -> str: """搜索数据库信息""" return f"关于'{query}'的搜索结果:示例数据" tools = [get_weather, search_database] llm_with_tools = chat_model.bind_tools(tools) response = llm_with_tools.invoke("上海天气如何?") if response.tool_calls: for tc in response.tool_calls: print(f"调用工具: {tc['name']},参数: {tc['args']}") for tool in tools: if tool.name == tc['name']: print(f"结果: {tool.invoke(tc['args'])}")

Agent中使用工具

如果希望工具选择、调用与结果整合全部自动化,可以将工具交由Agent管理。Agent会根据输入自动选择工具、执行调用,并结合结果继续推理直至生成最终答案。

from langchain.agents import create_agent from langchain_core.tools import tool @tool def multiply(a: int, b: int) -> int: """Multiply two numbers.""" return a * b agent = create_agent( model=chat_model, tools=[multiply] ) response = agent.invoke({ "messages": [ { "role": "user", "content": "12乘以9等于多少" } ] }) print(response["messages"][-1].content)

使用StructuredTool

除了使用@tool快速将函数封装为Tool外,还可以通过StructuredTool显式定义工具的名称、描述和参数结构,其中@tool更适合快速开发,StructuredTool更适合需要严格控制元信息的场景,但两者本质上都是将外部能力封装为模型可调用的Tool。

""" StructuredTool 单独演示 """ from langchain_core.tools import StructuredTool # 使用StructuredTool将函数转换为工具 multiply_tool = StructuredTool.from_function( func=multiply, name="multiply", description="Multiply two numbers" ) # 调用工具 result = multiply_tool.invoke({"a": 5, "b": 3})