数据截至 (上游 commit 92c146faa529)
Agent 单体与 chat 主循环
30 秒导读: 这一章只讲一件事——PraisonAI 里一个
Agent收到一句 prompt 后,自己这一轮是怎么转的: 怎么把角色、记忆、工具拼成 system prompt,怎么把预算算清楚不撑爆上下文,怎么发起 LLM 调用、 让模型决定是否调工具,再把结果拼回去继续问,直到模型给出最终答复。多个 Agent 之间怎么协作、 工具内部怎么安全执行、LLM provider 怎么分派——都不在本章,本章只讲一个 Agent 自己的一轮。
相关章节:总览见 index;工具的定义/注册/安全执行见 02-tools-and-mcp; LLM 层的双路径、多 provider 与容错见 03-llm-layer;多 Agent 编排见 04-multi-agent-orchestration。
1. 这是什么(零基础也能懂)
一句话定义: Agent 是 PraisonAI 的最小可用单体——你给它一个角色和一句话,它就能自己去想、
去调工具、再把工具结果读回来接着想,最后回你一段答复。
它解决什么问题: 裸调 LLM 只能"一问一答":你问,它答,完了。但真实任务往往是"你问 → 它得先查天气 →
拿到天气再算 → 才能答"。这中间"调工具、把工具结果喂回模型、再让模型接着说"的多轮编织,得有人来做。
Agent 就是做这件事的人。
用起来什么样: 最小的一次真实调用长这样。
from praisonaiagents import Agent
agent = Agent(instructions="你是一个乐于助人的助手")
answer = agent.chat("用一句话解释什么是黑洞") # 同步、阻塞,返回字符串
print(answer)
带工具时也只是多传一个 tools=[...],chat 内部会自动完成"模型要求调工具 → 执行 → 回填 → 再问"这一套:
def get_weather(city: str) -> str: # 普通 Python 函数就是工具
return f"{city} 今天晴,26℃"
agent = Agent(instructions="你是天气助手", tools=[get_weather])
print(agent.chat("北京今天适合出门吗?")) # 模 型会先调 get_weather,再据此回答
一句话直觉: 把 Agent 想成一个会用工具的接线员。你说一句话,它先在脑子里(system prompt)记好
"我是谁、我手上有哪些工具、我记得关于你的什么",然后开始打电话(调 LLM);电话那头(模型)如果说
"我需要先查一下 X",接线员就去查(执行工具),把查到的结果再念回电话里,直到那头说"好了,答案是……"。
本节到此不碰任何底层代码。下面开始拆它怎么转。
2. 顶层全景(它大概怎么转)
2.1 一轮 chat 的主干流水
先看一整轮从 agent.chat("...") 到拿到字符串,数据都流经哪些环节。怎么读这张图:从上到下是时间顺序,
右侧标注了真实方法名与所在行。
agent.chat(prompt) chat_mixin.py:1910
│
┌───────────────┼───────────────┐
│ ① 预处理:slash 技能展开 / 注入 steering 消息 / 若配置了外部 backend 则直接委派
└───────────────┼───────────────┘
│
_chat_impl(...) chat_mixin.py:1974
│
┌───────────────┼───────────────┐
│ ② 组装:多模态附件 / 模板 / 知识检索(RAG)拼进 prompt;user 消息先入历史
└───────────────┼───────────────┘
│
┌────────┴────────┐ ← 这里岔成两条路(见第 5 节)
│ │
_using_custom_llm 路径 openai_client 路径
(LLM 实例 .get_response) _chat_completion(...) chat_mixin.py:909
│ │
│ ┌────────┴────────┐
│ │ ③ 算上下文预算 → 需要就压缩 _compute_context_budget_and_route:677
│ │ ④ BEFORE_LLM 钩子 / 预算硬闸
│ │ ⑤ 派发:_chat_completion_with_retry → dispatcher
│ └────────┬────────┘
│ │
└────────┬────────┘
│ ⑥ dispatcher 内部完成 "调工具↔再问" 循环(下沉,见第 5.3 节)
│
┌───────────────┼───────────────┐
│ ⑦ 收尾:自反思(可选)→ 护栏校验 → 回调/展示 → AFTER_AGENT 钩子 → 落历史
└───────────────┼───────────────┘
│
返回 最终字符串
一句话概括:chat 负责"接住这一句"、_chat_impl 负责"把这一轮编织出来"、_chat_completion 负责
"把一次 LLM 调用发好并处理容错",而"调工具-回填-再问"的循环本身被交给了 LLM 层的 dispatcher。
2.2 部件与职责
| 部件(方法/属性) | 干什么 | 位置 |
|---|---|---|
Agent 类 | 由一堆 mixin 拼成的单体;chat/achat 是对外主入口 | src/praisonai-agents/praisonaiagents/agent/agent.py:292 |
__init__ | 把几十个配置参数归一化成运行期状态(模型、工具、记忆、执行策略…) | agent/agent.py:634 |
_build_system_prompt | 把角色/目标/记忆/技能/工具清单/安全提示拼成系统提示 | agent/chat_mixin.py:331 |
_build_messages | 把 system + 历史 + 本轮 user 拼成 messages 列表 | agent/chat_mixin.py:736 |
_format_tools_for_completion | 把各种形态的工具统一成 OpenAI function схема | agent/chat_mixin.py:827 |
chat / _chat_impl | 同步主入口 / 主循环实现 | agent/chat_mixin.py:2891 / :1974 |
_chat_completion | 发起一次 LLM 调用,含预算/钩子/成本/容错 | agent/chat_mixin.py:1728 |
_execute_unified_chat_completion | 把调用交给统一 dispatcher(工具循环下沉处) | agent/chat_mixin.py:2309 |
_compute_context_budget_and_route | 调用前算 token 预算,决定是否压缩 | agent/chat_mixin.py:1296 |
iter_stream / _start_stream | 面向应用的流式迭代器 | agent/chat_mixin.py:4519 / :3365 |
achat / _achat_impl | 上面这套的异步孪生 | agent/chat_mixin.py:3589 / :2563 |
3. Agent 是怎么"拼"出来的(mixin 组合 + 庞大 init)
3.1 一个类,十二个 mixin
Agent 本身几乎不直接写方法,而是把能力横切成十来个 mixin,再用多重继承拼起来。真源码就一行:
class Agent(GoalLoopMixin, SteeringMixin, SandboxMixin, SkillReviewMixin, UnifiedExecutionMixin,
ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin,
ExecutionMixin, MemoryMixin, AsyncMemoryMixin):
依据:src/praisonai-agents/praisonaiagents/agent/agent.py:292。
为什么这么拆? 单个 agent.py 会膨胀到无法维护,于是把"聊天""工具执行""记忆""会话""沙箱"各自
拆进独立文件的 mixin,agent.py 顶部再逐个 from .chat_mixin import ChatMixin 引入
(agent/agent.py:15-31)。本章关心的一轮对话,主要住在 ChatMixin(agent/chat_mixin.py)里。
MRO(方法解析顺序)从左到右,越左越"外层"。和本章相关的几个:
| Mixin | 负责本章哪一块 | 文件 |
|---|---|---|
ChatMixin | chat/_chat_impl/_chat_completion/流式/预算 | agent/chat_mixin.py |
UnifiedExecutionMixin | 统一 dispatcher 的创建与调用胶水 | agent/unified_execution_mixin.py |
ToolExecutionMixin | execute_tool(本章的下界,详见第 02 章) | agent/tool_execution.py |
SteeringMixin | chat 开头的 steering 消息注入 | agent/message_steering.py |
3.2 __init__:把"配置"熨平成"状态"
Agent.__init__ 的签名极长(agent/agent.py:634 起),但它的职责单一:把用户五花八门的传参归一化。
它的设计约定是"每个特性参数三态":False=关、True=用默认、传 Config 对象=自定义。签名注释写得很清楚:
# Each follows: False=disabled, True=defaults, Config=custom
依据:agent/agent.py:666-668。像 memory、knowledge、context、execution 都遵循这个三态约定
(agent/agent.py:670-680)。本章只需记住:__init__ 跑完后,self.llm、self.tools、self.chat_history、
self.execution、self._memory_instance 等运行期字段就位,之后每次 chat 都读它们。
一个关键的性能取舍:重依赖是懒加载的。HookRunner、StreamEventEmitter、LLM 显示工具都做成属性/惰性
loader,不用就不导入——注释说这把静默模式的导入时间从 ~420ms 压到 ~20ms(agent/agent.py:36-40、
属性 _hook_runner 在 agent/agent.py:317)。