Administrator
发布于 2026-05-14 / 0 阅读
0

lang架构的agent开发 3

Easy-lang的学习笔记

本章主要从状态管理、外部行动维度来学习实现agent的带记忆、能交互外部系统的能力。

3.1 Memory(状态管理层):让大模型具有记忆功能

虽然每一次的llm调用时memory都是无状态,独立的,但我们可以通过结构化的方式存储、管理对话历史,让AI具备“记忆能力”。

主要有两个核心的动作:

  • 存储(Save):将每一轮的用户输入(HumanMessage)和AI输出(AIMessage)保存到指定存储介质(内存、数据库等)。
  • 提取(Load):新一轮对话时,从存储介质中提取历史对话,注入到Prompt中供LLM参考。

这里主要学习的是LCEL架构,主要实现为三种记忆形式:

  • 全量记忆:完整保存所有对话历史,适用于短对话场景
  • 窗口记忆:仅保留最近N轮对话,控制Token消耗
  • 摘要记忆:通过LLM生成对话摘要替代完整历史,平衡上下文连贯性与效率

全量记忆

全量记忆适合短对话

# 1. 定义提示词模板(包含历史消息占位符)
full_memory_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是友好的对话助手,需基于完整的历史对话回答用户问题。"),
    MessagesPlaceholder(variable_name="chat_history"),  # 历史消息占位符
    ("human", "{user_input}")  # 用户当前输入
])

# 2. 构建基础链(提示词 + LLM)
base_chain = full_memory_prompt | llm

# 3. 会话历史存储(内存模式,生产环境可替换为数据库存储)
full_memory_store = {}

# 4. 定义会话历史获取函数(核心:返回完整历史)
def get_full_memory_history(session_id: str) -> BaseChatMessageHistory:
    """根据session_id获取会话历史,不存在则创建新的历史记录"""
    if session_id not in full_memory_store:
        full_memory_store[session_id] = InMemoryChatMessageHistory()
    return full_memory_store[session_id]

# 5. 构建带全量记忆的对话链
full_memory_chain = RunnableWithMessageHistory(
    runnable=base_chain,
    get_session_history=get_full_memory_history,
    input_messages_key="user_input",  # 输入中用户问题的键名
    history_messages_key="chat_history"  # 传入提示词的历史消息键名
)

# 测试多轮对话(指定session_id=user_001,隔离不同用户)
config = {"configurable": {"session_id": "user_001"}}

# 第一轮对话
response1 = full_memory_chain.invoke({"user_input": "我叫小明,喜欢编程"}, config=config)
print("助手回复1:", response1.content)
# 输出示例:你好小明!编程是一项很有创造力的技能,你平时常用什么编程语言呢?

# 第二轮对话(验证记忆:询问历史信息)
response2 = full_memory_chain.invoke({"user_input": "我刚才说我喜欢什么?"}, config=config)
print("助手回复2:", response2.content)
# 输出示例:你刚才说你喜欢编程呀~

# 查看完整历史记录
print("\n全量记忆的对话历史:")
for msg in get_full_memory_history("user_001").messages:
    print(f"{msg.type}: {msg.content}")

MessagesPlaceholder(variable_name="chat_history") :一个历史消息占位符。这是实现对话记忆的关键。它不在模板中写死任何内容,而是在程序运行时,动态地将之前的对话记录(比如用户之前问了什么,AI回答了什么)插入到这个位置。这样,AI在回答新问题时就能参考上下文,实现连贯的多轮对话

base_chain = full_memory_prompt | llm :使用管道操作符 |将两个组件连接起来,形成了一个简单的处理链,其含义是将前一个组件的输出,作为后一个组件的输入
其实就是拆分成三个流程:

    1. 执行prompt:
  • 2.管道执行:这个生成好的消息列表被自动传递给 llm
    1. 执行llm:根据收到的消息列表,生成一段连贯且符合上下文的回答。

执行结果:

助手回复1: 你好,小明!很高兴认识你。喜欢编程真棒,你平时更喜欢哪种编程语言,或者在做哪一类项目呢?
助手回复2: 你刚才说你喜欢编程。

全量记忆的对话历史:
human: 我叫小明,喜欢编程
ai: 你好,小明!很高兴认识你。喜欢编程真棒,你平时更喜欢哪种编程语言,或者在做哪一类项目呢?
human: 我刚才说我喜欢什么?
ai: 你刚才说你喜欢编程。

窗口记忆

只保留最近的N轮对话(N用k参数控制),早期的对话会自动丢弃,这样能有效控制文字量,适合客服、长期陪伴等长对话场景。

# 1. 定义提示词模板(与全量记忆通用,可复用)
window_memory_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是友好的对话助手,需基于最近的对话历史回答用户问题。"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{user_input}")
])

# 2. 构建基础链
window_base_chain = window_memory_prompt | llm

# 3. 会话历史存储
window_memory_store = {}
WINDOW_SIZE = 2  # 保留最近2轮对话(即最近4条消息:用户-助手-用户-助手)

# 4. 定义带窗口限制的会话历史获取函数
def get_window_memory_history(session_id: str) -> BaseChatMessageHistory:
    """获取会话历史,仅保留最近WINDOW_SIZE轮对话"""
    if session_id not in window_memory_store:
        window_memory_store[session_id] = InMemoryChatMessageHistory()
    
    # 获取完整历史,截取最近WINDOW_SIZE轮(每轮2条消息)
    history = window_memory_store[session_id]
    if len(history.messages) > 2 * WINDOW_SIZE:
        # 截取后WINDOW_SIZE轮消息(保留最新的)
        history.messages = history.messages[-2 * WINDOW_SIZE:]
    return history

# 5. 构建带窗口记忆的对话链
window_memory_chain = RunnableWithMessageHistory(
    runnable=window_base_chain,
    get_session_history=get_window_memory_history,
    input_messages_key="user_input",
    history_messages_key="chat_history"
)
# 测试多轮对话(session_id=user_002,与全量记忆会话隔离)
config = {"configurable": {"session_id": "user_002"}}

# 模拟5轮对话,验证窗口记忆的截断效果
inputs = [
    "我叫小红",
    "我喜欢画画",
    "我来自上海",
    "我是一名学生",
    "我刚才说我来自哪里?",  # 第5轮:询问第3轮的信息,验证窗口截断
    "我叫什么名字?"  # 第6轮:询问第1轮的信息,验证窗口记忆
]

for i, user_input in enumerate(inputs, 1):
    response = window_memory_chain.invoke({"user_input": user_input}, config=config)
    print(f"\n第{i}轮 - 助手回复:", response.content)

# 查看窗口记忆的最终历史(仅保留最近2轮)
print("\n窗口记忆的最终对话历史(最近2轮):")
for msg in get_window_memory_history("user_002").messages:
    print(f"{msg.type}: {msg.content}")

执行结果


我刚才称呼你“小红”是不准确的,如果你愿意,可以告诉我你叫什么,我会记住在这段对话里使用。

窗口记忆的最终对话历史(最近2轮):
human: 我刚才说我来自哪里?
ai: 你刚才说你来自**上海**。
human: 我叫什么名字?
ai: 你还没有告诉我你的名字。  

我刚才称呼你“小红”是不准确的,如果你愿意,可以告诉我你叫什么,我会记住在这段对话里使用。

摘要记忆

“摘要记忆”:它不保存对话原文,而是用LLM把历史对话总结成一段简洁的摘要。既能保留核心信息,又能最大程度节省文字量,缺点是可能会丢失一些细节(比如具体的数字、名字)。

# 1. 定义摘要生成提示词(用于压缩对话历史)
summary_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是对话摘要助手,需简洁总结以下对话的核心信息(包含用户身份、偏好、关键问题等),不超过50字。"),
    ("human", "对话历史:{chat_history_text}\n请生成摘要:")
])

# 2. 构建摘要生成链(输入完整历史文本,输出摘要)
summary_chain = summary_prompt | llm

# 3. 定义对话记忆提示词(注入摘要而非完整历史)
summary_memory_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是友好的对话助手,需基于对话摘要回答用户问题,摘要包含核心上下文信息。"),
    ("system", "对话摘要:{chat_summary}"),  # 注入摘要
    ("human", "{user_input}")
])

# 4. 构建基础对话链(提示词 + LLM)
summary_base_chain = (
    RunnablePassthrough.assign(
        chat_summary=lambda x: summary_chain.invoke(
            {
                "chat_history_text": "\n".join(
                    [f"{msg.type}: {msg.content}" for msg in x["chat_history"]]
                )
            }
        ).content
    )
    | summary_memory_prompt
    | llm
)

# 5. 会话历史存储(保存完整历史用于生成摘要)
summary_memory_store = {}

# 6. 定义会话历史获取函数
def get_summary_memory_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in summary_memory_store:
        summary_memory_store[session_id] = InMemoryChatMessageHistory()
    return summary_memory_store[session_id]

# 7. 构建带摘要记忆的对话链
summary_memory_chain = RunnableWithMessageHistory(
    runnable=summary_base_chain,
    get_session_history=get_summary_memory_history,
    input_messages_key="user_input",
    history_messages_key="chat_history"  # 传入完整历史用于生成摘要
)

# 测试多轮对话(session_id=user_003)
config = {"configurable": {"session_id": "user_003"}}

# 多轮对话输入
inputs = [
    "我叫小李,是一名产品经理",
    "我负责一款电商APP的迭代",
    "最近在优化用户下单流程",
    "遇到了用户流失率高的问题",
    "你能给我一些优化建议吗?"
]

for i, user_input in enumerate(inputs, 1):
    response = summary_memory_chain.invoke({"user_input": user_input}, config=config)
    print(f"\n第{i}轮 - 助手回复:", response.content)

# 查看完整历史与最终摘要
history = get_summary_memory_history("user_003")
print("\n摘要记忆的完整对话历史:")
for msg in history.messages:
    print(f"{msg.type}: {msg.content}")

# 单独生成最终摘要验证
final_summary = summary_chain.invoke({
    "chat_history_text": "\n".join([f"{msg.type}: {msg.content}" for msg in history.messages])
}).content
print(f"\n最终对话摘要:{final_summary}")
# 输出示例:摘要:小李,产品经理,负责电商APP迭代,优化下单流程时遇用户流失率高问题,寻求建议。

执行结果比较长,然后我这里就不展示了

不同窗口的总结

image.png|697

优化建议以及原理

image.png

主要记忆流程:用户新问题 → 记忆组件提取历史对话 → 把“历史+新问题”拼起来 → 发给LLM → LLM生成回复 → 把“新问题+回复”存到记忆里 → 输出结果
主要的两个插件组合:

  1. ChatMessageHistory:相当于“记忆笔记本”,负责具体的存和取操作。
  2. RunnableWithMessageHistory:相当于“记忆调度员”,负责协调整个流程——在调用LLM前自动取历史,调用后自动存新对话。

3.2 外部行动层(Tool):让AI能“动手”解决问题

Tool组件就是给AI装上“手和脚”,让它能通过调用外部工具,解决了它只能回答不能操作的问题。

Tool工具调用就是让AI学会“思考→行动→反馈”的循环,从简单的问答助手脱离成一个可以帮你干活的智能体:

  • 思考:自己能不能回答,要不要调用一些工具
  • 行动:生成工具调用指令
  • 反馈:根据工具返回查询结果

主要是利用三个核心组件:

  • Tool(工具):具体的“干活工具”,比如查询天气的工具、读文件的工具。每个工具都有明确的名称和描述(AI就是通过描述知道该用哪个工具的);
  • 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. 环境
# ======================
load_dotenv()
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL")
llm = ChatOpenAI(
    api_key=API_KEY,
    base_url=BASE_URL,
    model="deepseek-chat",
    temperature=0.3,
)

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

这一份代码中采用了自定义工具(@tool装饰器)

输出:


最终回答:
北京今天晴,气温约 `-2~8℃`。出门注意早晚保暖。

@tool工具源码

(function)
def tool(
    *,
    description: str | None = None,          # 工具功能描述(Agent判断是否调用的核心依据)
    return_direct: bool = False,             # 是否直接返回工具结果(False:LLM整理后返回)
    args_schema: ArgsSchema | None = None,   # 参数校验模型(Pydantic BaseModel)
    infer_schema: bool = True,               # 是否自动从类型注解推导参数schema
    response_format: Literal['content', 'content_and_artifact'] = "content",  # 返回格式
    parse_docstring: bool = False,           # 是否解析docstring生成描述/参数信息
    error_on_invalid_docstring: bool = True  # docstring解析失败时是否抛异常
)

一些拓展工具

文件操作工具(FileManagementToolkit)

  • ReadFile:读取本地文件(txt、docx 等)
  • WriteFile:写入本地文件
  • ListDirectory:查看文件夹内容
  • DeleteFile(部分版本):删除文件

章节总结

核心知识点

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

后面的优化方向

  • 记忆层:用 Redis/PostgreSQL 替代内存存储,实现记忆持久化;
  • 工具层:增加权限控制、重试机制、异常捕获(如文件写入失败提示);
  • 性能层:缓存高频工具调用结果、限制记忆长度控制 Token 消耗;
  • 体验层:优化工具调用话术,让回答更自然。