按源码顺序带你精读 agent.py
1. 先说这份文件在项目里的位置
backend/app/agent/agent.py 是这个项目里 Agent 能力的核心实现文件。
如果你用一句话概括它,可以这样说:
它负责创建一个会调用工具的智能体,并提供普通响应和流式响应两种执行方式。
这个文件不是:
- 工具定义文件
- 路由文件
- 会话数据库文件
它更像:
Agent 的总装配车间 + 执行入口
所以你读这份文件时,要重点理解:
- Agent 是怎么被创建出来的
- 工具是怎么接进去的
- 聊天历史是怎么参与的
- 为什么它能流式返回
2. 先看顶部导入
源码开头:
import os
import json
import asyncio
from typing import List, Optional, AsyncGenerator
from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import BaseMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import BaseTool
from app.agent.agent_middleware import get_middleware
from app.agent.agent_tools import ...
from app.core.logger_handler import logger
from app.services import session_manager as sm
from app.utils.prompt_loader import load_prompt
这一段已经把 Agent 的核心组成暴露出来了。
2.1 AgentExecutor
这是 LangChain 里真正“驱动 Agent 跑起来”的执行器。
你可以把它理解成:
Agent 的运行引擎
只有模型和工具还不够,必须有执行器负责:
- 调模型
- 看模型要不要用工具
- 真正调用工具
- 再把工具结果喂回模型
- 最终拿到答案
2.2 create_tool_calling_agent
这个函数非常关键。
它意味着这个项目里的 Agent 不是普通聊天机器人,而是:
支持工具调用的 Agent
也就是说,模型可以在运行过程中决定:
- 要不要调用工具
- 调哪个工具
- 用什么参数调
2.3 ChatTongyi
说明 Agent 背后的大脑是通义模型。
模型本身负责:
- 理解用户问题
- 判断需不需要用工具
- 基于工具结果继续推理
2.4 ChatPromptTemplate 和 MessagesPlaceholder
这说明这个 Agent 的输入不是一段纯字符串,而是一个结构化聊天模板。
它会包含:
- system prompt
- 聊天历史
- 用户输入
- Agent 中间执行轨迹
2.5 BaseTool
说明 Agent 接收的是标准化工具对象,而不是随便一个 Python 函数。
2.6 agent_tools
说明工具本身定义在别处,这个文件只负责把它们组装进 Agent。
2.7 session_manager
说明这个 Agent 不是无状态的。
它会读取历史会话,也会把结果写回会话。
这就是多轮对话能力的来源之一。
3. 正式进入 AgentFactory
源码:
class AgentFactory:
这一步是整个文件最重要的设计点之一。
3.1 为什么作者要写工厂类
因为创建一个 Agent 不是一行代码能搞定的。
它需要准备:
- 模型
- 工具
- 中间件
- 系统提示词
- Prompt
- 执行器
如果把这些逻辑散落在各个函数里,会很乱。
所以作者把它们统一封装到 AgentFactory。
3.2 你可以怎么理解它
可以把 AgentFactory 理解成:
一个负责“生产 Agent”的工厂
每次请求要跑 Agent 时,都可以向它要一个新的执行器实例。
4. 精读 AgentFactory.__init__
源码核心:
def __init__(
self,
model: str = "qwen3-max",
api_key: Optional[str] = None,
default_tools: Optional[List[BaseTool]] = None,
default_middleware: Optional[List] = None,
default_system_prompt: Optional[str] = None,
):
self.model = model
self.api_key = api_key or os.getenv("CHAT_API_KEY")
self.default_tools = default_tools or self._get_default_tools()
self.default_middleware = default_middleware or self._get_default_middleware()
self.default_system_prompt = default_system_prompt or self._get_default_system_prompt()
4.1 这一步不是在创建 Agent
这一点很重要。
这里做的是:
先把 Agent 的默认配置准备好
而不是立刻创建一个正在运行的 Agent。
4.2 这里保存了哪些默认项
主要包括:
- 默认模型名
- 默认 API Key
- 默认工具
- 默认中间件
- 默认系统提示词
这说明作者的设计是:
先配好默认生产参数,真正执行时再按需创建新实例
这有助于:
- 复用配置
- 避免重复初始化逻辑
4.3 self.api_key 这里有个细节
这里读的是:
CHAT_API_KEY
但后面的 _create_chat_model 实际又读了:
ALIYUN_ACCESS_KEY_SECRET
从工程一致性看,这里略微有点重复或不统一。
对你学习来说,重点是知道:
Agent 最终还是要通过环境变量拿模型访问凭证
5. 精读 _get_default_tools
源码:
return [
rag_summary_tools,
get_weather_tools,
what_time_is_now,
get_user_info_tools,
reorder_documents_tools,
]
5.1 这一段非常重要
因为它定义了这个 Agent 的能力边界。
换句话说:
Agent 能做什么,本质上取决于它拥有哪些工具
5.2 这些工具分别代表什么能力
rag_summary_tools查知识库并总结get_weather_tools查天气what_time_is_now查当前时间get_user_info_tools获取用户信息reorder_documents_tools对文档进行重排序
5.3 最关键的是哪个工具
最关键的是:
rag_summary_tools
因为它说明:
这个项目把 RAG 封成了 Agent 的一个工具
这意味着:
- 用户问知识库问题时
- Agent 可以自己决定调用 RAG
这就是 RAG 和 Agent 在这个项目中的结合方式。
6. 精读 _get_default_middleware
源码:
return get_middleware()
6.1 为什么这里值得注意
虽然在当前文件中 default_middleware 没有进一步深度使用,但它体现了一个设计意图:
Agent 的执行流程未来可能还要插入中间层能力
比如:
- 统一日志
- 安全检查
- 调用拦截
- 指标统计
即使现在没有完全展开,它也说明作者是在按可扩展方向设计。
7. 精读 _get_default_system_prompt
源码:
return load_prompt('main_prompt')
7.1 这一步说明什么
说明 Agent 的系统提示词并不是写死在代码里,而是:
- 存在 Prompt 文件中
- 运行时动态加载
7.2 这为什么重要
因为 Agent 的行为高度依赖 system prompt。
比如 prompt 可以决定:
- 它优先回答还是优先用工具
- 工具调用要不要谨慎
- 回复语气和风格
- 碰到不确定情况怎么办
把它放到外部文件里,后期调试会方便很多。
8. 精读 _create_chat_model
源码核心:
return ChatTongyi(
model=custom_model or self.model,
api_key=api_key,
streaming=True,
top_p=0.7,
)
8.1 它的作用是什么
它负责真正创建 Agent 背后的聊天模型实例。
也就是:
Agent 的“决策大脑”
8.2 为什么设置 streaming=True
因为后面这个 Agent 支持流式输出。
如果模型本身不支持流式,后面就没法边生成边推给前端。
8.3 为什么允许 custom_model
这是为了扩展性。
意思是:
- 默认用工厂配置的模型
- 但也允许调用方临时换一个模型
这让 Agent 更灵活。
9. 精读 _create_prompt
源码:
return ChatPromptTemplate.from_messages([
("system", "{system_prompt}"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
这几行非常关键,几乎定义了 Agent 的输入结构。
9.1 ("system", "{system_prompt}")
这是系统提示词。
它决定 Agent 的基础行为。
9.2 MessagesPlaceholder(variable_name="chat_history")
这是聊天历史占位符。
意味着执行时会把历史消息插到这里。
这让模型知道:
- 之前聊过什么
- 当前问题是在什么上下文里问的
9.3 ("human", "{input}")
这是当前用户输入。
也就是本轮真正的问题。
9.4 MessagesPlaceholder(variable_name="agent_scratchpad")
这是 Agent 非常有特色的一部分。
你可以把它理解成:
Agent 的“工作草稿区”
这里会放入:
- 前面的工具调用轨迹
- 中间思考过程
- 工具观察结果
也就是说,Agent 不是每轮都“失忆地从头思考”,而是会把执行痕迹带回给模型继续推理。
10. 精读 create_agent_executor
源码核心:
chat_model = self._create_chat_model(custom_model)
prompt = self._create_prompt()
tools = custom_tools or self.default_tools
system_prompt = custom_system_prompt or self.default_system_prompt
agent = create_tool_calling_agent(chat_model, tools, prompt)
return AgentExecutor(
agent=agent,
tools=tools,
verbose=verbose,
return_intermediate_steps=return_intermediate_steps,
**kwargs
)
这是整个工厂类最核心的方法。
10.1 第一步:准备组件
它先准备:
- 模型
- prompt
- 工具
- 系统提示词
这一步就像装配前先把零件摆好。
10.2 第二步:创建 Agent
这里用:
create_tool_calling_agent
意思是:
让模型具备“看到任务后决定是否调用工具”的能力
10.3 第三步:创建执行器
最后再用 AgentExecutor 包一层。
执行器负责真正跑整条循环:
用户输入
-> 模型判断
-> 调工具
-> 获取结果
-> 继续判断
-> 最终输出
10.4 一个值得你注意的小细节
这里虽然算出了:
system_prompt = custom_system_prompt or self.default_system_prompt
但后面创建 Agent 时并没有直接把它传进 _create_prompt。
当前实现里,系统 prompt 实际是在执行 astream(...) 时通过输入变量传进去的。
也就是说,prompt 模板里用了 {system_prompt} 占位符,真正值是运行时给的。
这也是 LangChain 常见的写法。
11. 全局工厂实例 agent_factory
源码:
agent_factory = AgentFactory()
11.1 这句意味着什么
说明系统在模块加载时就创建了一个全局工厂配置。
注意:
- 这不是全局 AgentExecutor
- 这是全局 AgentFactory
也就是说:
共享的是“生产规则”,不是“执行状态”
这很重要,因为真正执行时仍然会创建新的 executor,避免状态污染。
12. 精读 get_agent_response 开头
源码开头:
async def get_agent_response(...):
try:
agent_executor = agent_factory.create_agent_executor(...)
12.1 这个函数的定位
它是:
非流式 Agent 调用入口
也就是:
- 外部给它 query 和 history
- 它返回完整结果和中间步骤
12.2 为什么每次都重新创建 executor
因为作者非常明确地想避免:
- 全局状态污染
- 上一次调用影响下一次调用
这对 Agent 尤其重要,因为 Agent 执行过程中会产生很多临时状态。
13. 精读 get_agent_response 的聊天历史构建
源码:
chat_history: List[BaseMessage] = []
if history:
from langchain_core.messages import HumanMessage, AIMessage
for user_msg, assistant_msg in history:
chat_history.append(HumanMessage(content=user_msg))
chat_history.append(AIMessage(content=assistant_msg))
13.1 为什么不能直接传普通字符串列表
因为聊天模型需要知道:
- 哪句话是用户说的
- 哪句话是 AI 说的
所以必须转成消息对象。
13.2 为什么这里是成对追加
因为数据库里保存的是:
(user_message, assistant_message)结构
Agent 需要的是标准聊天消息流,于是就转成:
HumanMessageAIMessage
这就是数据库会话和 LangChain 消息系统之间的桥梁。
14. 精读 get_agent_response 的执行过程
源码关键:
async for chunk in agent_executor.astream({
"input": query,
"chat_history": chat_history,
"system_prompt": agent_factory.default_system_prompt
}):
14.1 为什么这里用的是 astream
虽然这个函数最终返回的是完整结果,但内部仍然用了流式执行。
这说明作者希望:
- 在执行层面逐块接收输出
- 同时也能拿到中间步骤
14.2 传入了哪些关键变量
input当前用户问题chat_history历史消息system_prompt系统提示词
这些值会填进前面 _create_prompt 里定义的模板。
14.3 为什么这就是 Agent 的真正运行入口
因为从这一刻开始,LangChain 才真正接管流程:
- 模型读 prompt
- 决定是否调用工具
- 执行工具
- 返回中间结果
- 最终产出回答
15. 精读 get_agent_response 如何处理输出块
源码:
if "output" in chunk:
full_response.append(chunk["output"])
elif "intermediate_steps" in chunk:
...
15.1 output 是什么
这是模型最终生成的回答片段。
它可能是:
- 一整段
- 也可能是流式分块的一部分
代码里把它们逐块收集进 full_response,最后再拼接起来。
15.2 intermediate_steps 是什么
这是 Agent 的中间步骤。
通常包含:
- 模型的某次行动选择
- 选择的工具
- 工具输入
- 工具返回结果
这对理解 Agent 特别重要,因为它让 Agent 不再是一个黑盒。
15.3 为什么要把中间步骤保存下来
主要有两个价值:
- 方便调试
- 可以向前端或日志展示 Agent 的执行轨迹
这在复杂 Agent 系统中非常重要。
16. 精读 get_agent_response 的步骤记录
源码:
steps.append({
"thought": action.log,
"tool": action.tool,
"tool_input": action.tool_input,
"tool_output": observation
})
16.1 这段的意义
它把 Agent 的关键行为结构化保存了下来。
字段含义分别是:
thought模型这一步的思路或动作日志tool调用了哪个工具tool_input给工具传了什么参数tool_output工具返回了什么结果
16.2 为什么这很有价值
因为 Agent 最难调试的一点就是:
你不知道它为什么这么做
这套步骤记录会显著改善可解释性。
17. 精读 get_agent_response 的返回值
源码:
return {
"response": "".join(full_response) if full_response else "抱歉,我无法理解您的请求。",
"steps": steps
}
17.1 返回结构有什么特点
它不只返回最终回答,还返回执行步骤。
这说明这个函数定位不是“简单聊天接口”,而是:
一个既能给结果,也能给执行轨迹的 Agent 服务函数
17.2 为什么空响应要给兜底文案
因为模型可能没有正常产生输出。
这时至少给用户一个明确反馈,而不是返回空字符串。
18. 精读 get_agent_stream_response 开头
源码开头:
async def get_agent_stream_response(... ) -> AsyncGenerator[str, None]:
18.1 这个函数的定位
这是:
面向前端实时展示的流式 Agent 响应入口
也就是说,它不是一次性返回完整 JSON,而是持续 yield 数据块。
18.2 为什么返回 AsyncGenerator
因为它会边执行边产出数据,非常适合:
- SSE
- 流式聊天界面
19. 精读 get_agent_stream_response 的会话读取
源码:
history = await sm.session_manager.get_history(session_id, user_id)
19.1 这里在做什么
它先根据:
session_iduser_id
从会话管理器中读取历史记录。
19.2 为什么这一步重要
因为流式 Agent 不是单轮无状态问答。
它需要知道:
- 之前聊了什么
- 当前问题是不是接着上文来的
这就是多轮对话的关键。
20. 精读 get_agent_stream_response 的初始输出
源码:
yield f"data: {json.dumps({'type': 'response', 'content': '', 'session_id': session_id}, ensure_ascii=False)}\n\n"
20.1 为什么一开始先发一个空响应
这通常是为了让前端尽快知道:
- 流已经建立
- 当前会话 ID 是什么
你可以把它理解成:
“连接建立成功,现在开始准备正式内容”
20.2 为什么格式长这样
这是标准 SSE 风格:
data: ...
前端可以一条一条消费。
21. 精读 get_agent_stream_response 的流式主循环
源码核心:
async for chunk in agent_executor.astream({...}):
if "output" in chunk:
...
yield ...
elif "intermediate_steps" in chunk:
...
21.1 这里和非流式版本最大的区别是什么
区别在于:
- 非流式版本把片段收集起来,最后一次性返回
- 流式版本每拿到一段输出,就立刻
yield给前端
21.2 chunk["output"] 怎么处理
这里会:
- 保存到
full_response - 立刻通过 SSE 发给前端
- 打日志
await asyncio.sleep(0.05)
21.3 为什么加 sleep(0.05)
这是一个节奏控制技巧。
可能是为了:
- 减少过于碎片化的推送
- 让前端显示更平滑
- 让日志和输出节奏更稳定
21.4 intermediate_steps 在流式里为什么也要记录
因为即使面向前端实时输出,开发者仍然需要:
- 调试 Agent
- 回看工具调用轨迹
所以这部分保留了和非流式版本类似的记录逻辑。
22. 精读 get_agent_stream_response 的结果落库
源码:
response = "".join(full_response) if full_response else "抱歉,我无法理解您的请求。"
await sm.session_manager.add_message(session_id, user_id, query, response)
22.1 为什么要等流式完成后再写库
因为要写入的是完整回答。
如果中途就写:
- 可能写入半截内容
- 会话记录会不完整
所以正确方式是:
先流式给用户看,全部结束后再持久化完整答案
22.2 这一步说明什么
说明这个项目的流式输出和持久化并不是分离的两套逻辑,而是连在一起的:
- 前端实时看到内容
- 后端最终保存完整会话
这是一种很常见、也很合理的实现方式。
23. 精读 get_agent_stream_response 的结束标记
源码:
yield f"data: {json.dumps({'type': 'done', 'session_id': session_id}, ensure_ascii=False)}\n\n"
23.1 这个结束标记的作用
告诉前端:
- 这轮流式输出结束了
- 可以停止等待
- 可以更新 UI 状态
如果没有这个标记,前端可能不知道什么时候真正完成。
24. 精读异常处理
源码:
except Exception as e:
error_message = f"错误: {str(e)}"
yield ...
yield ...
24.1 流式场景为什么异常处理更重要
因为在流式输出里,连接已经开始了。
如果中途出错,不能简单 raise 让它炸掉。
而应该:
- 给前端发一个错误事件
- 再发一个结束事件
这样前端才能正确收尾。
24.2 这体现了什么工程意识
体现了:
流式接口不仅要考虑“成功怎么输出”,还要考虑“失败怎么优雅结束”
这点非常重要。
25. 把整份 agent.py 按执行顺序串起来
如果用户发来一个问题,这份文件里的整体执行逻辑大致是:
- 模块加载时创建全局
agent_factory - 工厂预加载默认模型名、工具、系统提示词等配置
- 请求到来时调用
get_agent_response或get_agent_stream_response - 工厂为这次请求创建一个新的
AgentExecutor - 读取并构造历史聊天消息
- 把
system_prompt、chat_history、input交给 Agent - Agent 根据 prompt 判断是否调用工具
- 若调用工具,则记录中间步骤
- 若是流式接口,就把输出片段实时
yield给前端 - 全部完成后拼接成完整响应
- 流式模式下将完整回答写入会话历史
- 向前端发送
done结束标记
这 12 步就是整份文件的主线。
26. 这份文件最值得你学的 6 个点
26.1 用工厂模式封装 Agent 创建逻辑
避免模型、工具、prompt 初始化逻辑散落一地。
26.2 用工具定义 Agent 能力边界
Agent 强不强,不只是看模型,更要看工具集。
26.3 用结构化聊天 Prompt 组织输入
system、history、input、scratchpad 分层很清楚。
26.4 每次请求创建全新执行器
减少状态污染风险。
26.5 记录中间步骤
提高 Agent 的可解释性和可调试性。
26.6 同时支持非流式和流式两种执行方式
既方便调试,又适合前端实时交互。
27. 你接下来该怎么继续看
如果你已经看懂这篇,最适合下一步配套去看:
backend/app/agent/agent_tools.py因为它决定 Agent 有哪些能力backend/app/router/chat.py因为它决定 Agent 如何通过 API 对外暴露backend/app/services/database_session_manager.py因为它决定多轮对话历史怎么读写backend/app/prompt/main_prompt.txt因为它直接影响 Agent 的行为风格
28. 最后一句话总结
agent.py 的本质不是“调一下模型”,而是:
把模型、工具、聊天历史、执行器和流式输出组织成一个可运行的 Agent 系统,让大模型不仅能回答问题,还能在需要时调用外部能力并持续返回结果。