2-DeepAgents快速入门与流式解析
2 - 深度研搜:DeepAgents 快速入门与流式解析
本章课程目标:
- 跑通第一个 DeepAgent:给智能体配置联网搜索工具,并用
invoke()获取最终结果。 - 理解
result["messages"]的结果结构,知道一次 Agent 执行过程中模型和工具分别做了什么。 - 掌握
stream()的基本使用方式,能解析 DeepAgents 的流式输出。 - 能区分“模型决策”“工具执行”和“最终回复”几类关键事件,并知道子智能体调度也可以作为后续扩展事件识别。
- 为下一章学习
subagents配置和主智能体调度做准备。
学习建议: 这一章先跑,再拆。先把最小示例执行成功,确认普通返回和 stream() 都能看到结果;再回头看 messages、chunk、tool_calls、content 这些字段分别从哪里来。DeepAgents 内部节点第一遍不用追到底,先能读懂流式输出里每一段代表什么。
对应代码分支: 02-quickstart-streaming
上一章我们已经理解了 DeepAgents 的定位:它适合处理长链路、复杂、多步骤、需要自主规划和分工的智能体任务。
从这一章开始,我们进入代码。先不急着做完整的「深度研搜」项目,而是先用一个最小示例理解 DeepAgent 的创建、执行和流式输出。
本章要完成的案例很简单:
1 | 用户提问:请查询人工智能和机器人领域的热门新闻信息,并整理为一份简要报告。 |
通过这个案例,你会看到一次 Agent 执行过程并不是“模型直接返回答案”,而是下面这条链路:
1 | 用户问题 |
这段流程图可以按“谁在做事”来读:用户只负责提出问题,模型负责判断下一步,工具负责拿外部资料,最后还是由模型把工具结果整理成人能读懂的报告。这条链路理解清楚后,后面加入子智能体、知识库、数据库、文件生成和 WebSocket 实时推送时,本质上都是在扩展它。
1、快速入门:创建第一个 DeepAgent
1.1 安装依赖
在运行代码之前,先准备 Python 环境和依赖。当前教学代码仓库 deepsearch-agents,已经使用 uv 管理 Python 环境和依赖。
如果你还不了解 uv,可以先参考「3 - 电商问数:开发环境与基础服务准备」中关于 uv 的入门说明。本章不再展开讲 uv 的基础概念,只需要知道:在这个项目里,依赖已经写在 pyproject.toml 中,第一次运行时直接同步即可。
当前 deepsearch-agents/pyproject.toml 中指定的 Python 版本是:
1 | requires-python = ">=3.12,<3.13" |
也就是说,本项目建议使用 Python 3.12。如果你已经安装好 uv,进入 deepsearch-agents 项目根目录后,执行:
1 | uv sync |
uv sync 会根据项目中的 pyproject.toml 和 uv.lock 自动创建 .venv,并安装锁定好的依赖版本。当前项目核心依赖如下:
| 依赖 | 当前版本 | 作用 |
|---|---|---|
deepagents |
0.5.7 |
创建 DeepAgent 的核心框架 |
langchain |
1.2.17 |
提供工具封装、消息结构和模型初始化入口 |
langchain-openai |
1.2.1 |
通过 OpenAI 兼容协议连接模型服务 |
langgraph |
1.1.10 |
DeepAgents 底层图执行与状态流转能力 |
python-dotenv |
1.2.2 |
从 .env 文件读取环境变量 |
tavily-python |
0.7.24 |
调用 Tavily 联网搜索 |
如果你后面需要新增依赖,再使用 uv add 包名;如果只是运行当前仓库中的教学示例,通常执行 uv sync 就可以了。
1.2 代码位置与执行步骤
项目对应文件路径:deepsearch-agents/examples/1-deep-agent-quickstart-search.py、deepsearch-agents/examples/2-deep-agent-streaming-chunks.py
其中,1-deep-agent-quickstart-search.py 用来演示非流式调用,2-deep-agent-streaming-chunks.py 用来演示流式解析。
代码执行顺序如下:
- 安装本章依赖。
- 配置
.env。 - 创建 Tavily 搜索客户端。
- 使用
@tool把普通 Python 函数封装成 Agent 可调用工具。 - 初始化大模型。
- 使用
create_deep_agent()创建 DeepAgent。 - 使用
invoke()非流式执行。 - 从
result["messages"][-1].content中取最终结果。
运行非流式示例:
1 | uv run examples/1-deep-agent-quickstart-search.py |
运行流式解析示例:
1 | uv run examples/2-deep-agent-streaming-chunks.py |
1.3 配置环境变量
代码会从 .env 中读取模型和 Tavily 搜索配置。实际开发时不要把真实 Key 写进文档,也不要提交到仓库中,建议使用下面这种占位方式:
1 | OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 |
1.4 封装联网搜索工具
DeepAgents 需要通过工具与外部世界交互。本章使用 TavilyClient 封装一个简单的互联网搜索工具。
1 | import os |
这里最关键的是 @tool 装饰器。它会把一个普通 Python 函数包装成 LangChain 工具,让 Agent 能够在推理过程中调用它。
工具的函数签名和注释也很重要。模型会根据工具名称、参数类型和描述判断什么时候该调用这个工具、应该传什么参数。如果工具描述太含糊,模型就更容易误用。
1.5 初始化模型
接下来初始化大模型对象:
1 | from langchain.chat_models import init_chat_model |
这里的 llm 是 DeepAgent 做判断和生成回复时使用的模型对象。无论是普通 Agent 还是 DeepAgent,都必须有模型参与决策。
1.6 创建 DeepAgent
创建 DeepAgent 的核心函数是 create_deep_agent()。
1 | from deepagents import create_deep_agent |
先看四个核心参数:
| 参数 | 含义 |
|---|---|
model |
智能体使用的大模型 |
tools |
主智能体可以直接调用的工具列表 |
subagents |
子智能体列表,本节先留空 |
system_prompt |
主智能体的系统提示词,用来定义角色、目标和工具使用边界 |
这一版代码中虽然 subagents=[],但它依然是 DeepAgent。只是当前示例先用一个搜索工具帮助我们理解 Agent 的执行过程,子智能体会在后面单独讲。
1.7 先用 invoke 跑通结果
最小示例先使用 invoke() 执行:
1 | # 非流式执行,invoke 会等整条 agent 链路完成后,一次性返回最终状态 |
注意入参不是简单字符串,而是一个包含 messages 的字典:
1 | { |
这是因为 DeepAgents 底层沿用了 LangGraph / LangChain 的消息模型,整个执行过程都会围绕 messages 传递状态。
执行文件验证,成功:
1 | uv run examples/1-deep-agent-quickstart-search.py |
其中,LangChainPendingDeprecationWarning 是 LangGraph / LangChain 依赖内部的版本提示,不影响本章示例运行。真正要观察的是后面的工具调用日志、messages 轨迹和最终报告。
这段输出可以按三层来看:
| 输出位置 | 说明 |
|---|---|
开始调用网络搜索工具... |
工具函数内部的 print(),说明 internet_search 被真正执行 |
result["messages"] |
完整执行轨迹,包含用户消息、模型工具调用、工具返回和最终回答 |
| 最后的 Markdown 报告 | result["messages"][-1].content,也就是最终展示给用户的内容 |
这里先建立一个直觉:invoke() 虽然最后拿到的是一份报告,但它内部并不是“一次模型回答”,而是经历了“模型决定调工具 -> 工具返回结果 -> 模型整理答案”的完整链路。
2、非流式调用:invoke 与 messages
2.1 实际调用解析
invoke() 返回的不是一段纯文本,而是一个包含完整消息过程的字典。简化后可以理解成这样:
1 | result = { |
也就是说,一次 Agent 执行大致经历下面几步:
- 用户把问题发给 Agent。
- Agent 把用户问题、系统提示词、工具描述一起交给模型。
- 模型判断需要调用
internet_search。 - Agent 真正执行搜索工具。
- 工具返回搜索结果。
- Agent 再把工具结果交给模型。
- 模型整理搜索结果,生成最终报告。
这 7 步背后,其实有四个角色在配合:
1 | 用户 -> Agent -> 大模型 -> 工具 -> Agent -> 大模型 -> 用户 |
第一步,用户并不是直接调用大模型,而是把问题交给 deep_agent:
1 | result = deep_agent.invoke( |
这一步传入的是 messages,DeepAgents 会先把这条用户消息包装成 HumanMessage,放进本次执行状态中。
第二步,Agent 会把下面几类信息一起交给大模型:
- 用户问题:
请查询人工智能和机器人领域的热门新闻信息,并整理为一份简要报告。 - 系统提示词:你是一名严谨的研究员,可以使用搜索工具生成结构清晰、信息可靠的中文报告
- 工具说明:当前可用工具是
internet_search - 工具参数结构:
query、max_results、topic、include_raw_content
也就是说,大模型并不是“天然知道”Agent 有哪些工具,而是 Agent 在调用模型时,把工具名称、参数和描述一起放进上下文里。模型读完这些信息后,才会判断:这个问题需要联网搜索,于是生成一次工具调用。
这一步对应 result["messages"] 里的第二条消息:
1 | AIMessage( |
这里要注意:这条 AIMessage 还不是最终回答。它的 content 为空,但 tool_calls 有值,意思是模型在告诉 Agent:“下一步请调用 internet_search 工具,并传入这些参数。”
第三步,Agent 根据 tool_calls 真正去执行工具,也就是调用我们前面定义的 Python 函数:
1 |
|
工具执行完成后,会生成一条 ToolMessage。这条消息里保存的是 Tavily 返回的原始搜索结果,通常是结构化数据,里面可能包含标题、链接、摘要、发布时间等信息。
结合实际输出,ToolMessage(content=...) 里的 JSON 主要包含这些字段:
| 字段 | 含义 |
|---|---|
query |
本次传给 Tavily 的搜索关键词 |
results |
搜索结果列表 |
url |
单条搜索结果的网页链接 |
title |
单条搜索结果的标题 |
content |
单条搜索结果的摘要内容 |
published_date |
新闻发布时间 |
response_time |
Tavily 本次请求耗时 |
request_id |
Tavily 本次请求 ID |
第四步,工具结果不会直接返回给用户。Agent 会把 ToolMessage 再交给大模型,让模型根据搜索结果进行整理、翻译、筛选和润色。比如 Tavily 返回的结果可能包含英文网页摘要,但用户提问是中文,所以最终结果通常会被模型整理成中文报告。
这一步对应最后一条 AIMessage:
1 | AIMessage( |
到了这里,tool_calls 已经没有值,content 有了完整内容,说明模型不再继续调用工具,而是已经生成最终回复。
所以我们取最终回复时,通常直接取最后一条消息:
1 | final_answer = result["messages"][-1].content |
如果只关心最终报告,读取最后一条消息即可;如果要调试 Agent 的执行链路,再查看完整的 result。
2.2 为什么模型知道该调用哪个工具
它是 Agent 工具调用的核心。
模型本身并不会主动扫描你的 Python 代码,也不会自己知道项目里有哪些函数。真正发生的是:当我们通过 create_deep_agent() 创建智能体时,已经把工具列表传给了 Agent。
1 | deep_agent = create_deep_agent( |
执行 invoke() 时,Agent 会把 internet_search 的名称、描述和参数结构整理给模型。模型读完以后,才会决定是否生成 tool_calls。
因此,工具调用的判断链路可以这样记:
1 | 工具注册到 Agent |
这也解释了为什么前面一直强调工具函数的名称、参数类型和注释要写清楚。它们不是只给开发者看的,也会影响模型能不能正确调用工具。
2.3 读懂四类消息
为了读懂 result["messages"],可以先记住下面这张表:
| 消息类型 | 在执行链路中的作用 | 常见特征 |
|---|---|---|
HumanMessage |
用户输入的问题 | content 是用户原始问题 |
AIMessage |
模型的思考结果,可能是调用工具,也可能是回复 | 有 tool_calls 时表示准备调工具 |
ToolMessage |
工具真实执行后的返回结果 | name 通常是工具名 |
AIMessage |
模型基于工具结果整理出的最终回答 | content 有最终自然语言结果 |
其中最容易混淆的是 AIMessage。它有两种情况:
AIMessage(content="", tool_calls=[...]):模型还没有回答,它只是决定下一步调用什么工具。AIMessage(content="..."):模型已经生成最终回答。
所以判断 Agent 到底在干什么,不能只看消息类型,还要继续看 tool_calls 和 content。
3、流式调用:stream 与 chunk
3.1 为什么要改用 stream
invoke() 属于非流式执行。它的特点是调用后一直等待,直到整个智能体任务执行完,再一次性返回完整结果。
1 | 用户点击发送 -> 后端执行 5 秒或 10 秒 -> 一次性返回最终结果 |
这种方式写起来简单,但用户体验不够好。尤其是 DeepAgents 这类长链路任务,智能体可能会搜索、阅读、调用多个工具、分派子任务。如果前端一直没有反馈,用户很难判断系统是在处理,还是已经卡住。
流式执行的目标就是让用户看到过程:
1 | 开始处理 |
后面做 Web 项目时,这些过程通常会通过 SSE 或 WebSocket 推送给前端。本项目完整版本采用的是 WebSocket,因为它更适合持续推送 Agent 的执行状态和工具日志。
3.2 用 stream 获取执行过程
项目对应文件路径:deepsearch-agents/examples/2-deep-agent-streaming-chunks.py
把 invoke() 换成 stream() 后,返回值就不再是完整结果,而是一个可以不断迭代的流:
1 | # 流式执行,stream 会在每个图节点完成后产出一个 chunk |
每个 chunk 都表示某个节点刚刚产生了一次状态更新。由于 DeepAgents 底层基于 LangGraph,流式输出会带有节点信息。
简化来看,常见的 chunk 可能长这样:
1 | { |
或者:
1 | { |
解析流式输出时,重点看两层信息:
- 外层节点名:当前是
model节点,还是tools节点。 - 内层最后一条消息:模型是决定调用工具,还是已经给出最终回答。
3.3 chunk 的四种关键状态
| 状态 | 如何识别 | 含义 |
|---|---|---|
| 模型决定调用工具 | node_name == "model" 且 last_msg.tool_calls 中工具名不是 task |
模型判断下一步要调用某个普通工具 |
| 模型决定调用子智能体 | node_name == "model" 且 tool_call["name"] == "task" |
模型判断下一步要把任务分派给某个子智能体 |
| 工具执行完成 | node_name == "tools" |
Agent 真正调用工具,并拿到了工具返回结果 |
| 模型返回最终结果 | node_name == "model" 且没有 tool_calls,但 last_msg.content 有内容 |
模型基于前面结果生成最终回复 |
本章的示例只配置了 internet_search,所以运行时主要会看到“普通工具调用”。表格中的子智能体状态是先给后面留一个入口:等第 3 章真正配置 subagents 后,流式输出里就可能出现这类调度事件。
3.4 chunk 四种状态的对象视图示例
只看上面的表格还比较抽象。下面参考项目文档中的写法,把 stream() 迭代过程中可能出现的几类 chunk 展开成 Python 对象视图。
注意,这里不是要求你手写这些对象,而是帮助你在 print(chunk) 时能快速识别当前 Agent 运行到哪一步。
场景 A:模型决定调用普通工具。
本章代码里最常见的就是这一类。模型读完用户问题后,发现需要联网搜索,于是生成一个 tool_calls,工具名是 internet_search。
1 | { |
这个状态的重点是:content 为空,tool_calls 有值。说明模型还没有回答用户,而是在告诉 Agent:“下一步请调用这个工具。”
场景 B:工具执行完成并返回结果。
当 Agent 真正执行完 internet_search 后,会从 tools 节点产出 ToolMessage:
1 | { |
这个状态的重点是:外层节点名变成了 tools。此时不是模型在思考,而是工具已经执行完,并把原始结果交回给 Agent。
场景 C:模型决定调用子智能体。
本章还没有真正配置子智能体,所以这类状态通常不会在当前示例里出现。但后面第 3 章加入 subagents 后,如果模型决定把任务委派给子智能体,就会看到特殊工具 task:
1 | { |
task 本质上也是一次工具调用,只是它调用的不是普通 Python 工具,而是 DeepAgents 的子智能体调度入口。解析时只要看到 tool_call["name"] == "task",就可以把它识别成“正在分派给子智能体”。
场景 D:模型生成最终回复。
当工具结果已经返回,模型完成整理后,会再次从 model 节点产出消息。这一次没有 tool_calls,而是有真正的 content:
1 | { |
这个状态说明 Agent 链路已经收束,模型不再继续调用工具或子智能体,而是把最终结果返回给用户。
除了上面四类业务事件,实际输出中还可能看到一些中间件节点,例如 PatchToolCallsMiddleware.before_agent、TodoListMiddleware.after_model。这些节点可能不包含 messages,或者不是我们要展示给用户的业务进度,所以后面的解析代码会用下面这句过滤掉:
1 | if not state or "messages" not in state: |
3.5 解析 chunk 的完整代码
项目对应文件路径:deepsearch-agents/examples/2-deep-agent-streaming-chunks.py
1 | # 流式执行,stream 会在每个图节点完成后产出一个 chunk |
这段代码里有几个细节要特别注意。
**第一,**外层循环取的是 node_name 和 state:
1 | for node_name, state in chunk.items(): |
node_name 用来判断当前是哪类节点产生了输出,例如 model 或 tools。state 里才是真正的状态数据。
**第二,**有些中间件节点不包含 messages,要先跳过:
1 | if not state or "messages" not in state: |
DeepAgents 内部可能会出现一些中间件节点,例如 TodoListMiddleware.after_model。它们对框架执行有意义,但不一定是我们要展示给用户的业务事件。
**第三,**只取 messages[-1]:
1 | last_msg = messages[-1] |
因为每个状态更新里可能包含多条消息,但当前这次更新最关键的一条通常在最后。
第四,model 节点要继续区分两类情况:
last_msg.tool_calls有值:模型不是最终回答,而是在决定下一步调用什么。last_msg.content有值:模型已经生成最终回复。
第五,tools 节点代表工具已经真的执行完了。这里可以把工具名和返回结果截断后推送给前端,避免一大段搜索结果把界面刷满。
执行文件验证,成功:
1 | uv run examples/2-deep-agent-streaming-chunks.py |
这段输出比 invoke() 更适合观察执行过程,可以按顺序读:
| 输出片段 | 对应的执行阶段 |
|---|---|
【大模型】决定调用工具... |
model 节点输出,模型决定下一步调用 internet_search |
开始调用网络搜索工具... |
Python 工具函数真正开始执行 |
【agent】调用了internet_search工具... |
tools 节点输出,工具结果已经返回 |
【大模型】最终执行的结果... |
model 节点再次输出,模型基于工具结果生成最终回答 |
这就是流式解析最核心的价值:不用等到最终报告生成,开发者和前端都能看到 Agent 当前走到了哪一步。
3.6 对接前端时怎么展示
虽然本章还没有写 FastAPI 接口,但可以提前建立后端事件和前端展示之间的对应关系。
| 后端解析出的事件 | 前端可以展示成什么 |
|---|---|
模型决定调用工具 internet_search |
正在搜索网络资料 |
| 工具返回搜索结果 | 已获取搜索结果,正在整理 |
| 模型决定调用子智能体 | 正在分派给某个专业助手 |
| 模型返回最终结果 | 展示最终答案或报告 |
这也是为什么流式解析很重要。企业级智能体项目不只看最终答案,还要让用户能感知执行过程,开发者也能定位每一步做了什么。
一句话总结: invoke() 看最终结果,stream() 看执行过程;做真实产品时,流式处理几乎是必须掌握的能力。
本章小结:
这一章我们完成了 DeepAgents 的快速入门。先定义了 internet_search 搜索工具,再初始化模型,最后通过 create_deep_agent() 组装主智能体,并用 invoke() 获取最终结果。
接着我们重点学习了 stream() 的流式解析方式。只要抓住 model 和 tools 两类节点,再结合 tool_calls 和 content,就能判断当前到底是模型在做决策、工具在执行,还是模型已经生成最终结果;后面加入子智能体后,也是在这套解析逻辑上继续扩展。
完整的子智能体配置、调度信号和异步执行,会在下一章展开。你现在只需要先掌握这一层:invoke() 适合看最终结果,stream() 适合看清 Agent 每一步正在做什么。