Skip to main content

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——一段”工具说明书”,模型读得懂。
然后他调模型,看到了一段让他起鸡皮疙瘩的输出——模型第一轮返回的不是回答,是”工具调用申请单”:
小林愣住了:“模型没回答,它在’申请’——它说’我要调 get_weather 查北京天气’。它不会真的执行,它在等我的代码去执行。” 他这才明白工具调用的本质:模型是”指挥官”,你是”执行者”。模型决定”调哪个、传什么参”,你的代码真的去跑函数,把结果回传。
想验证”申请单”吗?跑 10_tools.py——第一轮模型返回带 tool_calls 的 AIMessage(content 是 tool_use 块),你执行工具后把结果作为 role: "tool" 消息回传,第二轮模型给出最终回答。三步:模型申请 → 你执行 → 结果回传。

第三幕:Agent——把”三步舞”自动化

小林现在会手动做工具循环了,但他很快发现:如果模型想连调 3 个工具,他要手写 3 轮循环;如果模型想调了工具再调工具,他得写 while 循环。这太累了。 他查到了 LangChain 的官方定义,差点没笑出声:
Agent = Model + Harness(模型 + 鞍具)。模型负责思考,harness 负责工具循环、消息管理、记忆。
create_agent 就是那个”鞍具”——你只管给它模型和工具:
然后他看到了 create_agent 最令人震撼的地方——它自己会循环调工具,直到给出最终回答:
小林打印了 result["messages"],发现这是一个完整的”流水账”:
他恍然大悟:result["messages"] 本身就是历史! 把它原样传回下一轮,记忆和工具就同时有了:
想验证”自动循环 + 记忆”吗?跑 10_agent.py——三问收官:① 复合问题(自动连调两个工具)② 记忆测试(“我刚问的哪个城市”,它记得)③ 数学计算(自动调 calculate 工具)。

🔧 技术细节:这一章涉及的关键类和签名

@tool 装饰器——把函数变成”模型能读的工具说明书”(langchain_core.tools)
BaseTool——工具基类的关键方法
create_agent——把”三步舞”自动化的鞍具(langchain.agents)
④ agent 返回结构——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 这个黑盒里好像还藏着什么结构……

复现信号:什么时候你会想起这一章

  1. 想让模型”记得用户”——历史 + session_id 隔离;或直接用 Agent(messages 本身就是历史)。
  2. 想让模型”干活”(查库、算数、查天气)——@tool 定义工具 + bind_tools,模型返回 tool_calls 你执行。
  3. 想让模型”自主决定调哪些工具、调几次”——create_agent(model, tools, system_prompt)
  4. 报错”tool_calls 为空”或”模型不调工具”——不是代码错,是模型自主决定;检查工具说明书写清楚没有。
小林的助手能记住、能干活了。但小林总觉得 create_agent 是个黑盒——他不知道模型”下一步干嘛”、“循环到哪了”、中途能不能插手。老板还问:“这个机器人能不能在我确认之后再执行操作?” 小林知道,他需要真正的”图”了——翻到第 11 篇:从流水线到会转弯的图