1. 为什么「带记忆的多工具 Agent」是面试硬通货
大模型应用开发已经过了“会调 API 就能写简历”的阶段。现在面试官真正想考察的是:你能否让模型在真实任务中自主决策、调用外部工具、并且记住上下文持续工作。这三点分别对应三个核心技术:
- ReAct 架构:Reasoning + Acting 的循环,让模型在“思考”和“行动”之间交替,而不是一次性输出答案。
- LangGraph:把 Agent 的决策流程建模成状态图,比传统的 AgentExecutor 更灵活、更可控。
- 工具调用(Tool Calling):让模型能查天气、搜网页、执行代码、读写数据库,突破纯文本生成的边界。
再加一层记忆——多轮对话里模型要记住用户之前说过什么、执行过什么,这就是“带记忆的多工具 Agent”。这套组合拳在面试里几乎必考,而且通常要求现场手撕代码。
本文会用一段可以复制运行的 Python 代码,完整实现一个带记忆的多工具 Agent。读完你就能直接拿去面试。
核心概念
在正式进入代码之前,先建立三个关键概念的共识,它们是理解后文所有示例的基础:
- Agent(智能体):以大模型为决策中枢,能够根据目标自主规划步骤、调用外部工具并持续迭代执行的应用形态。它不是简单的“问答机器人”,而是一个会“干活”的执行体。
- ReAct 架构:Reasoning + Acting 的缩写,让模型在“推理”与“行动”之间交替循环。模型先思考下一步,再决定是调用工具还是输出最终答案,直到任务完成。
- 工具调用(Tool Calling):模型输出结构化指令、由程序实际执行外部工具并返回结果的能力。它让 Agent 从“只能生成文本”扩展为“能查数据、能计算、能操作外部系统”。
这三个概念叠加起来,再加上贯穿多轮对话的记忆,就构成了本文要手撕的完整 Agent。
2. ReAct 架构:模型为什么要「边想边做」
2.1 传统方式的问题
如果只用一个简单的提示词让模型回答问题,模型遇到“北京明天天气怎么样”这种需要实时数据的问题时,只能凭训练数据编一个答案。这不是能力问题,而是它没有获得外部信息的手段。
2.2 ReAct 的核心循环
ReAct 的核心是一个循环:
每一轮循环包含两个动作:
- Reasoning(推理):模型分析当前状态,决定下一步是调用工具还是直接回答。
- Acting(行动):模型生成一个结构化的工具调用,由程序真正执行工具,再把结果拼回上下文。
这个循环会一直持续,直到模型认为信息足够、输出最终答案为止。
2.3 一个最小的 ReAct 提示词
REACT_PROMPT="""你是一个智能助手,可以使用以下工具: {tools} 请按照以下格式回答: Thought: 你的思考过程 Action: 要调用的工具名 Action Input: 工具的输入参数 Observation: 工具执行结果 ...(Thought/Action/Action Input/Observation 可以重复多次) Thought: 我现在知道最终答案了 Final Answer: 给用户的最终回答 用户问题:{question} """这个提示词就是 ReAct 的“灵魂”。它强制模型按照固定的节奏输出,程序再解析Action和Action Input,真正调用工具。现在很多框架已经把这个过程封装好了,但面试官问你 ReAct 原理时,你要能讲清楚这个循环。
3. LangGraph 核心概念:把 Agent 画成一张图
3.1 为什么不用 LangChain 的 AgentExecutor
早期 LangChain 的AgentExecutor是一个黑盒循环,流程写死在框架里。想加记忆、加人工确认、加条件分支都很别扭。LangGraph 的思路完全不同:把 Agent 的每个环节都拆成一个节点(Node),用边(Edge)连接起来,形成一个显式的状态图。
好处有三个:
- 流程可视化:Agent 的每一步都看得见,出问题好排查。
- 高度可控:可以随时在图里插入人工审批、重试、超时等节点。
- 状态可持久化:图的状态就是记忆的载体,天然支持多轮对话。
3.2 三个核心概念
| 概念 | 作用 | 类比 |
|---|---|---|
State | 在节点之间传递的数据结构 | 流水线上的工件 |
Node | 一个处理函数,接收 State 并返回更新 | 流水线上的工位 |
Edge | 决定下一个节点是谁,可以带条件 | 流水线上的传送带 |
一个最简单的图长这样:
fromtypingimportTypedDict,Annotatedfromlanggraph.graphimportStateGraph,START,ENDclassAgentState(TypedDict):messages:Annotated[list,"messages"]defnode_a(state:AgentState):# 处理逻辑,返回要更新的字段return{"messages":state["messages"]+["A 处理完成"]}graph=StateGraph(AgentState)graph.add_node("a",node_a)graph.add_edge(START,"a")graph.add_edge("a",END)app=graph.compile()3.3 条件边:ReAct 循环的图表达
ReAct 里“要不要继续调工具”是一个判断,这在 LangGraph 里用条件边表达:
defshould_continue(state:AgentState)->str:last_message=state["messages"][-1]# 如果最后一条消息包含工具调用,就回到工具节点ifgetattr(last_message,"tool_calls",None):return"tools"# 否则结束returnEND graph.add_conditional_edges("agent",should_continue,{"tools":"tools",END:END})这就是 LangGraph 表达 ReAct 循环的完整方式:agent节点负责推理,tools节点负责执行工具,条件边决定“继续做”还是“收工”。整个循环在图的状态里自动运行,直到走出END。
4. 实战:手撕一个带记忆的多工具 Agent
下面是一段完整可运行的代码。我们用langchain-openai作为模型层,工具是两个常用场景:查询天气和执行简单计算。整个 Agent 有完整的四轮对话记忆,并且每轮都能调用不同工具。
4.1 安装依赖
pipinstalllanggraph langchain-openai langchain-core4.2 定义工具
fromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的天气。 参数: city: 城市名称,例如"北京" """weather_data={"北京":"晴,28°C,空气质量优","上海":"多云,26°C","广州":"雷阵雨,32°C","深圳":"晴,30°C",}returnweather_data.get(city,f"暂无{city}的天气数据")@tooldefcalculate(expression:str)->str:"""执行一个数学表达式计算。 参数: expression: 数学表达式,例如 "123 * 45 + 67" """try:result=eval(expression,{"__builtins__":{}},{})returnf"计算结果为:{result}"exceptExceptionase:returnf"计算失败:{e}"说明:示例中
eval使用了受限的命名空间,仅用于教学演示。生产环境请替换为ast解析或专用计算库,避免任意代码执行风险。
4.3 定义带记忆的 State
记忆的本质是把历史消息保存在 State 里,并且使用add_messages归并器来追加而不是覆盖:
fromtypingimportTypedDict,Annotatedfromlangchain_core.messagesimportBaseMessagefromlanggraph.graph.messageimportadd_messagesclassAgentState(TypedDict):# add_messages 保证了每轮消息会追加到列表末尾,而不是覆盖messages:Annotated[list[BaseMessage],add_messages]4.4 绑定工具并构建图
importosfromlangchain_openaiimportChatOpenAIfromlanggraph.graphimportStateGraph,START,ENDfromlanggraph.prebuiltimportToolNode# 初始化模型model=ChatOpenAI(model="gpt-4o-mini",temperature=0,api_key=os.getenv("OPENAI_API_KEY"),)# 把工具列表绑定到模型tools=[get_weather,calculate]model_with_tools=model.bind_tools(tools)defagent_node(state:AgentState):"""agent 节点:模型根据当前消息决定调用工具还是直接回答。"""response=model_with_tools.invoke(state["messages"])return{"messages":[response]}# 构建图defbuild_agent():graph=StateGraph(AgentState)graph.add_node("agent",agent_node)# ToolNode 会自动解析最后一条消息里的 tool_calls 并执行工具graph.add_node("tools",ToolNode(tools))defshould_continue(state:AgentState)->str:last_message=state["messages"][-1]ifgetattr(last_message,"tool_calls",None):return"tools"returnEND graph.add_edge(START,"agent")graph.add_conditional_edges("agent",should_continue,{"tools":"tools",END:END})graph.add_edge("tools","agent")returngraph.compile()app=build_agent()4.5 四轮对话验证记忆与工具切换
fromlangchain_core.messagesimportHumanMessage,AIMessage# 记忆列表会贯穿所有轮次memory:list[BaseMessage]=[]# 第一轮:查天气memory.append(HumanMessage(content="北京今天天气怎么样?"))result=app.invoke({"messages":memory})memory=result["messages"]print("第一轮:",memory[-1].content)# 第二轮:计算题(验证工具切换)memory.append(HumanMessage(content="请帮我算一下 1024 * 768 等于多少?"))result=app.invoke({"messages":memory})memory=result["messages"]print("第二轮:",memory[-1].content)# 第三轮:追问天气(验证多轮记忆)memory.append(HumanMessage(content="那上海呢?"))result=app.invoke({"messages":memory})memory=result["messages"]print("第三轮:",memory[-1].content)# 第四轮:验证模型记得上下文memory.append(HumanMessage(content="我刚才问了哪几个城市?"))result=app.invoke({"messages":memory})memory=result["messages"]print("第四轮:",memory[-1].content)第三轮的“那上海呢?”最能体现记忆的价值:模型必须回顾前文才知道用户是在问天气,而不是问别的。第四轮则直接考验模型对整段对话的回忆能力。
5. 面试高频追问:这样回答才加分
5.1 “ReAct 和 Function Calling 是什么关系?”
参考答案:Function Calling 是模型层的能力,指模型可以输出结构化的工具调用请求;ReAct 是应用层的循环架构,它利用 Function Calling 来驱动“思考—行动—观察”的循环。两者不在一个层面:Function Calling 是“手”,ReAct 是“怎么用手”。
5.2 “为什么用 LangGraph 而不是直接写 while 循环?”
参考答案:while 循环也能实现 ReAct,但 LangGraph 提供了三样东西:一是状态管理,图的 State 天然支持持久化、断点恢复和回溯;二是可视化与可观测性,每个节点的输入输出都能追踪;三是可扩展性,加人工审批、并行分支、超时重试都只需要改图结构,不需要改动核心循环代码。
5.3 “记忆存在哪?怎么持久化?”
参考答案:短期记忆存在 State 的messages列表里,进程内就能跑;持久化需要给graph.compile()传入checkpointer,比如MemorySaver(内存)或SqliteSaver(磁盘),这样每次invoke都传入thread_id,就能恢复对应会话的完整状态。跨会话的长期记忆还可以单独挂一个向量库,把历史对话存成 embedding,检索后拼进提示词。
5.4 “工具调用失败了怎么办?”
参考答案:分两层处理。第一层是模型层面,在工具描述里写清楚异常返回格式,模型拿到错误信息后会自己调整策略;第二层是图层面,用ToolNode的异常捕获或者自定义 try/except 包裹节点,把工具异常转成一条普通消息返回给模型,让它继续决策,避免整个 Agent 崩溃。
5.5 “怎么控制 Agent 不无限循环?”
参考答案:三个手段:一是设定max_iterations,超过就强制结束并返回中间结果;二是在should_continue里加判断,连续 N 次调用同一个工具且输入没变化就终止;三是给模型绑定max_tokens和时间预算。这些控制逻辑在图的条件边里实现非常自然。
6. 总结
这一阶段的目标不是“会用 LangGraph”,而是能从零手写出一个带记忆的多工具 Agent,并讲清楚每一层为什么这样设计。梳理一下核心链路:
面试时如果被要求手撕,就按本文的节奏来:先定义工具,再定义带add_messages的 State,然后构建agent+tools两个节点,最后用条件边串成循环。逻辑清晰、结构完整,就是硬通货该有的样子。