10 · 会记住、会干活的助手:Memory、Tools 与 Agent
开头的现象
老板试用小林的 RAG 问答机,问了一句:“我上个月问过你年假的事,你还记得吗?” 问答机回答:“我们好像是第一次对话。” 老板皱起眉头。小林在旁边擦了擦汗,心里明白:RAG 是无状态的,每次提问都是”翻手册”,它根本不记得”这个用户刚才问过什么”。 更麻烦的还在后面。老板又说:“小林,让它顺便帮我查一下明天北京天气,再算一下 1024×768 等于多少——它一个都干不了,只会说’我是文本助手’。” 小林看着那个只会”照着念手册”的问答机,心里冒出一个念头:“它缺两样东西——‘记住’和’干活’。而这两样,正好是第 4 篇的记忆和第 1 篇的模型都解决不了的。我得找新工具。“第一幕:记忆的”正经做法”——手写历史 + 会话隔离
小林第 4 篇学会了手写历史:history.append(resp) 滚雪球。但在 RAG 问答机上,这个雪球有个问题——每次检索、每次新会话,历史都得重新拼。
他想要的是:把”存历史”这件事抽出来,按用户(session_id)自动管理,每个用户各记各的。
纯 langchain_core 就能做到——用 InMemoryChatMessageHistory 当”存储抽屉”:
session_id 的概念,脑子里亮了一下:“这就像浏览器的多标签页——每个标签页(session_id)有自己的浏览记录,互不串门。用户 A 的对话历史,不会污染用户 B 的。”
他记下了一个判断:记忆的本质 = 历史消息 + 会话隔离。 第 4 篇的手写历史是”手动管理”,InMemoryChatMessageHistory 是”按会话分抽屉存”,LangGraph 的 checkpointer 是”生产级:自动存 + 持久化到磁盘”(第 11 篇的事)。前两个用 langchain_core 就能做,第三个要 LangGraph。
想验证”会话隔离”吗?跑上面这段代码——⚠️ 小林的提醒:你可能在网上看到get_history("user1")和get_history("user2")各是各的抽屉。给 user1 存三句话,user2 的messages还是空的。
RunnableWithMessageHistory——那是官方已废弃的组件(1.3.3 标记 deprecated,2.0 移除)。别学它,直接用上面的手写 + InMemory 方案,或者直接上 LangGraph 的 checkpointer。
第二幕:工具——让模型”有手”
记忆搞定了”记住”,接下来是”干活”。小林想要模型能查天气、能算数。他一开始天真地想:“给模型写 if-else?” 他写了几行就放弃了——模型可能问一百种方式,你不可能穷举。 他翻到了@tool 装饰器,文档说:“把普通 Python 函数变成’模型能看懂的工具说明书’。”
get_weather 这个工具长什么样,发现 @tool 干了一件神奇的事:它把函数的名字、docstring、参数类型(city: str)翻译成了一段 JSON Schema——一段”工具说明书”,模型读得懂。
想验证”申请单”吗?跑10_tools.py——第一轮模型返回带tool_calls的 AIMessage(content 是 tool_use 块),你执行工具后把结果作为role: "tool"消息回传,第二轮模型给出最终回答。三步:模型申请 → 你执行 → 结果回传。
第三幕:Agent——把”三步舞”自动化
小林现在会手动做工具循环了,但他很快发现:如果模型想连调 3 个工具,他要手写 3 轮循环;如果模型想调了工具再调工具,他得写 while 循环。这太累了。 他查到了 LangChain 的官方定义,差点没笑出声:Agent = Model + Harness(模型 + 鞍具)。模型负责思考,harness 负责工具循环、消息管理、记忆。而
create_agent 就是那个”鞍具”——你只管给它模型和工具:
result["messages"],发现这是一个完整的”流水账”:
result["messages"] 本身就是历史! 把它原样传回下一轮,记忆和工具就同时有了:
想验证”自动循环 + 记忆”吗?跑 10_agent.py——三问收官:① 复合问题(自动连调两个工具)② 记忆测试(“我刚问的哪个城市”,它记得)③ 数学计算(自动调 calculate 工具)。
🔧 技术细节:这一章涉及的关键类和签名
①@tool 装饰器——把函数变成”模型能读的工具说明书”(langchain_core.tools)
BaseTool——工具基类的关键方法
create_agent——把”三步舞”自动化的鞍具(langchain.agents)
result["messages"] 就是历史
想验证吗?get_weather.args是一个 dict(自动从类型注解推断);print(type(agent))显示CompiledStateGraph。
第四幕:边界——Agent 不是银弹
小林用了一天,摸清了 Agent 的边界: 它能干的:自主决定调哪个工具、调几次、什么时候收手;带记忆(messages 回传);整合多个工具结果。 它不能干的 / 代价:- 模型决定一切,你控制不了过程——它可能不调工具直接答(你没法强制它”必须调”),也可能调了没用的工具。
- 循环可能失控——如果工具一直返回”还有事要做”,模型会一直调下去(有次数上限保护,但成本会涨)。
- 工具越多越容易”选择困难”——工具描述写得模糊,模型就乱调。工具说明书(docstring)质量直接决定 agent 智商。
- 不是所有任务都该用 Agent——固定流程(检索→生成)用 Chain(第 9 篇的 RAG)就够了,Agent 是”流程会变”才用。
想验证边界吗?给模型绑一个工具但不说明”什么时候该用”,问一个根本不需要工具的问题——模型可能完全不调工具直接答(它觉得没必要)。这就是”模型自主”的代价。
结论(小林用一天换来的)
记忆 = 历史消息 + 会话隔离(session_id);工具 =@tool 把函数变成”说明书”,模型只负责”申请”(tool_calls),你负责执行;Agent = create_agent 把这套循环自动化——模型思考,harness 干活,result["messages"] 就是历史,传回去就有记忆。 边界:Agent 让模型自主决策,但”自主”意味着”不可控”;固定流程别用 Agent。** 小林现在有了”会记住、会干活的助手”——但他隐隐觉得,create_agent 这个黑盒里好像还藏着什么结构……
复现信号:什么时候你会想起这一章
- 想让模型”记得用户”——历史 + session_id 隔离;或直接用 Agent(messages 本身就是历史)。
- 想让模型”干活”(查库、算数、查天气)——
@tool定义工具 +bind_tools,模型返回 tool_calls 你执行。 - 想让模型”自主决定调哪些工具、调几次”——
create_agent(model, tools, system_prompt)。 - 报错”tool_calls 为空”或”模型不调工具”——不是代码错,是模型自主决定;检查工具说明书写清楚没有。