支持联网搜索的智能代理系统
上一篇的快递流程是「画好路线照着走」的工作流。本篇让流程带点 Agent 味道:让大模型自己决定要不要调用搜索工具 。
我们会实现两个核心节点(调模型、执行工具),并认识 LangGraph 预构建的两大利器------ToolNode 和 tools_condition,最后演示图的可视化和流式输出。
目录
- 案例流程设计
- 准备工作:模型、搜索工具、绑定工具
- 定义状态:messages 与计数
- 节点一:llm_call 调用大模型
- 回顾 tool_calls 与 ToolMessage
- 节点二:tool_node 执行工具
- 条件边:should_continue 与 tools_condition
- 完整代码
- 图可视化与流式输出
- 本篇小结
1. 案例流程设计
一个能联网的助手,流程是一个「思考 → 行动」的循环:
START → llm_call(调模型)
│
├─ 模型要搜索 → tool_node(执行搜索)→ 回到 llm_call(带着结果再答)
│
└─ 模型直接答完 → END
模型每次回答后,我们看它有没有「请求调用工具」:有就去执行工具、把结果喂回模型;没有就结束。
2. 准备工作:模型、搜索工具、绑定工具
用 init_chat_model 准备模型,用 Tavily 做联网搜索,再用 bind_tools 把工具绑给模型:
python
from langchain.chat_models import init_chat_model
from langchain_tavily import TavilySearch
# 模型
model = init_chat_model("gpt-4o-mini", temperature=0)
# 搜索工具,max_results 控制返回几条结果
search = TavilySearch(max_results=4)
tools = [search]
# 关键:把工具绑定给模型,模型才知道自己能调用哪些工具
model_with_tools = model.bind_tools(tools)
需要安装:
pip install -U langchain-tavily,并配置 Tavily 的 API Key(环境变量TAVILY_API_KEY)。
3. 定义状态:messages 与计数
状态里最核心的是 messages(消息列表) ,它承担三个作用:对话记忆、上下文维护、状态持久化 。用 operator.add 让新消息追加而不是覆盖:
python
from typing_extensions import TypedDict
from typing import Annotated
import operator
from langchain.messages import AnyMessage
class State(TypedDict):
# 消息列表:所有对话和工具结果都在这里,逐次追加
messages: Annotated[list[AnyMessage], operator.add]
# 统计模型被调用了多少次
llm_calls: int
说明:State 里存进去的消息,运行时会被自动反序列化成 LangChain 的消息对象,所以可以用「点属性」访问,例如
state["messages"][-1].tool_calls。
4. 节点一:llm_call 调用大模型
给模型一个系统提示(告诉它可以调用搜索),把历史消息一起传进去:
python
from langchain.messages import SystemMessage
def llm_call(state: State) -> dict:
print("【调用模型】")
response = model_with_tools.invoke(
[SystemMessage(content="乐于助人的助手,支持调用工具进行搜索")]
+ state["messages"]
)
return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1,
}
5. 回顾 tool_calls 与 ToolMessage
当模型决定搜索时,返回的 AI 消息(AIMessage)里会带一个 tool_calls 字段,结构大致是:
python
AIMessage(
content="",
tool_calls=[
{
"name": "tavily_search", # 要调用哪个工具
"args": {"query": "西安天气"}, # 传给工具的参数
"id": "call_xxxx", # 这次调用的唯一 ID
}
],
)
工具执行完,要构造一条 ToolMessage(工具消息) 把结果还回去,并且用 tool_call_id 对应上是哪次调用:
python
ToolMessage(
content="工具返回的结果",
tool_call_id="call_xxxx", # 必须和 AIMessage.tool_calls 里的 id 对应
)
6. 节点二:tool_node 执行工具
遍历最后一条 AI 消息里的 tool_calls,逐个执行对应工具,把结果包成 ToolMessage:
python
from langchain.messages import ToolMessage
# 工具名 → 工具对象,方便按名字找到工具
tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: State) -> dict:
print("【执行工具】")
results = []
# 取最后一条 AI 消息里的所有工具调用
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]] # 找到工具
observation = tool.invoke(tool_call["args"]) # 执行工具
results.append(
ToolMessage(
content=observation,
tool_call_id=tool_call["id"], # 对应调用 ID
)
)
return {"messages": results}
7. 条件边:should_continue 与 tools_condition
7.1 自己写路由函数
判断最后一条消息有没有工具调用:有就去 tool_node,没有就 END:
python
from langgraph.graph import END
def should_continue(state: State) -> str:
last_message = state["messages"][-1]
if last_message.tool_calls: # 模型请求调用工具
return "tool_node"
return END # 模型答完,结束
7.2 用预构建的 tools_condition(更省事)
LangGraph 已经内置了同样逻辑的 tools_condition ,它返回 "tools" 或 "__end__":
python
from langgraph.prebuilt import tools_condition
同样,执行工具也可以直接用预构建的 ToolNode,不必自己写 tool_node:
python
from langgraph.prebuilt import ToolNode
tool_node = ToolNode(tools) # 直接传入工具列表即可
8. 完整代码
python
from typing_extensions import TypedDict
from typing import Annotated
import operator
from langchain.chat_models import init_chat_model
from langchain.messages import AnyMessage, SystemMessage, HumanMessage
from langchain_tavily import TavilySearch
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
# 1. 模型与工具
model = init_chat_model("gpt-4o-mini", temperature=0)
search = TavilySearch(max_results=4)
tools = [search]
model_with_tools = model.bind_tools(tools)
# 2. 状态
class State(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int
# 3. 节点
def llm_call(state: State) -> dict:
response = model_with_tools.invoke(
[SystemMessage(content="乐于助人的助手,支持调用工具进行搜索")]
+ state["messages"]
)
return {"messages": [response], "llm_calls": state.get("llm_calls", 0) + 1}
tool_node = ToolNode(tools)
# 4. 构建图
builder = StateGraph(State)
builder.add_node("llm_call", llm_call)
builder.add_node("tool_node", tool_node)
builder.add_edge(START, "llm_call")
builder.add_conditional_edges(
"llm_call",
tools_condition, # 预构建路由
{"tools": "tool_node", "__end__": END},
)
builder.add_edge("tool_node", "llm_call") # 工具结果返回模型
graph = builder.compile()
# 5. 调用
result = graph.invoke(
{"messages": [HumanMessage(content="今天西安天气怎么样?")], "llm_calls": 0}
)
print(result["messages"][-1].content)
9. 图可视化与流式输出
9.1 可视化图结构
编译后的图可以导出 Mermaid 流程图,xray=True 显示更详细的内部结构:
python
image = graph.get_graph(xray=True).draw_mermaid_png()
with open("graph.png", "wb") as f:
f.write(image)
也可以配合 matplotlib 直接显示,或把 Mermaid 文本贴到在线 Mermaid 工具里渲染。
9.2 流式输出 stream
用 stream() 可以按图的执行步骤逐步返回,每个 chunk 是一个字典:键是节点名,值是该节点将要更新的状态:
python
for chunk in graph.stream(
{"messages": [HumanMessage(content="今天西安天气怎么样?")], "llm_calls": 0}
):
print(chunk)
这样能清楚看到「llm_call 一步、tool_node 一步」的实时执行过程。流式的完整玩法后面有专篇详解。
10. 本篇小结
- 搜索 Agent 是一个「llm_call ⇄ tool_node」的循环:模型要工具就执行、结果喂回,不要工具就结束。
- bind_tools 把工具绑给模型;模型想调用时,AIMessage 里会带 tool_calls(name/args/id)。
- 工具结果要包成 ToolMessage ,并用
tool_call_id对应调用 ID。 - 状态核心是 messages ,用
operator.add追加,承担对话记忆、上下文、持久化三件事。 - 预构建利器:ToolNode(自动执行工具)和 tools_condition(自动判断要不要去工具节点),能省掉大量样板代码。
get_graph().draw_mermaid_png()可视化,stream()按步骤流式执行。
下一篇我们做更贴近实战的案例三:代理式 RAG 系统------让 AI 自己决定是否检索、对检索结果做质量打分,不合格就重写问题重新检索,形成一个质量闭环。