QwenPaw Agent 实现原理深度剖析

对应代码路径: src/qwenpaw/agents/

一、整体架构

继承层次 (MRO)

scss 复制代码
QwenPawAgent
  └── CodingModeMixin         (编码模式: 注入编码提示词、注册 LSP/AST 工具)
        └── ToolGuardMixin    (工具安全守卫: 拦截敏感工具调用 + 审批流程)
              └── ReActAgent  (AgentScope 框架的 ReAct 循环引擎)
                    └── AgentBase (AgentScope 基类)

这个继承层次是理解整个 Agent 行为的关键。 从底向上看,AgentBase 是 AgentScope 框架提供的最底层基类,定义了 Agent 的最基本契约------能接收消息、能回复消息。ReActAgent 在基类之上实现了完整的 ReAct(Reasoning + Acting)循环引擎:它是一个 while 循环,每次迭代先让 LLM "思考"(_reasoning),如果 LLM 返回了工具调用请求,就执行工具(_acting),然后把工具结果放回上下文,继续下一轮推理。这个循环一直持续到 LLM 决定直接回复文本,或者达到最大迭代次数。

ToolGuardMixinCodingModeMixin 是两个 Mixin 类,它们的唯一目的就是_acting 这个方法上插入拦截逻辑ToolGuardMixin 负责安全检查------每次 LLM 想调用工具时,它先检查这个工具有没有被禁用、是否越界、是否含注入代码、需不需要人工审批。CodingModeMixin 在编码模式下注册 IDE 类工具(LSP 语言服务、AST 搜索),让 Agent 能理解代码结构、跳转定义、查找引用。

顶层的 QwenPawAgent 是实际使用的 Agent 类,它把所有能力组装到一起:工具注册、技能加载、提示词构建、模型创建、钩子注册、命令处理。三个层次各司其职,互不干扰。

为什么用 Mixin 而不是组合? 因为 _actingReActAgent 的核心方法,ReAct 循环中每次工具调用都会调用它。Mixin 方式通过 Python 的 MRO(方法解析顺序,即 C3 线性化算法)让子类 override 这个方法后调用 super()._acting() 形成处理链------每个 Mixin 只处理自己关心的事,然后透传给下一个。组合模式需要在外部写一个包装类显式转发每个方法,做不到这种对核心循环的透明拦截。这里选 Mixin 不是图方便,而是因为拦截点就这一个方法,Mixin 是表达这种"洋葱中间件"语义最自然的 Python 方式。

模块文件一览

文件/目录 作用
react_agent.py (1477行) 核心 Agent 类 --- 初始化、工具注册、技能加载、系统提示词、ReAct 重写
coding_mode_mixin.py 编码模式 Mixin --- 注入编码提示词、注册 LSP/AST 工具
tool_guard_mixin.py (810行) 安全守卫 Mixin --- 工具调用拦截、审批流程、自动拒绝
command_handler.py (748行) 系统命令处理 --- /compact, /new, /clear, /summary
model_factory.py (1075行) 模型工厂 --- 创建模型实例 + Formatter
prompt.py 提示词构建 --- 从 AGENTS.md / SOUL.md / PROFILE.md 加载
skill_system/ 技能系统 --- 技能注册、池管理、工作区同步
hooks/ 钩子系统 --- 包含 BootstrapHook(首次交互引导)
memory/ 记忆系统 --- 向量检索记忆、文件记忆、PG 记忆
context/ 上下文管理 --- 轻量级上下文管理器(智能截断)
tools/ 内置工具实现 --- 文件、Shell、浏览器、桌面截图等
skills/ 内置技能定义 --- PDF、DOCX、新闻等(每个技能一个目录)
utils/ 工具函数 --- 文件处理、Token 计数、消息处理

二、Agent 初始化流程 (init)

Agent 的初始化是整个系统中逻辑最密集的阶段之一。它不只是 new 一个对象那么简单------在构造函数执行完毕时,Agent 已经完成了工具注册、技能加载、提示词构建、模型创建、记忆系统对接、命令解析、钩子挂载等一整套准备工作。理解这个初始化流程,就理解了 Agent 所有能力的来源。

当 AgentRunner 创建 QwenPawAgent 时,__init__ 按顺序执行以下步骤。这个顺序不是随意的------每一步都依赖于前面步骤产生的对象(例如:只有先解析出可用技能列表,才能注册技能对应的工具;只有先构建好 toolkit,才能初始化 ReActAgent 父类):

scss 复制代码
QwenPawAgent.__init__()
  │
  ├── 1. 保存基础属性
  │   ├── _agent_config     → Agent 配置对象
  │   ├── _env_context      → 环境上下文文本
  │   ├── _request_context  → 请求元数据 (session_id, user_id, channel, agent_id)
  │   ├── _mcp_clients      → MCP 客户端列表
  │   ├── _workspace_dir    → 工作区目录
  │   └── _task_tracker     → 任务追踪器
  │
  ├── 2. 解析适用技能
  │   ├── ensure_skills_initialized(workspace_dir)     → 确保技能目录就绪
  │   ├── resolve_effective_skills(workspace_dir, channel)
  │   │   → 根据 channel 筛选启用的技能列表
  │   └── 例如: console 渠道可能启用所有技能,telegram 渠道只启用部分
  │
  ├── 3. 创建工具注册表 (Toolkit)
  │   └── _create_toolkit(namesake_strategy, effective_skills)
  │       ├── 添加所有内置工具 (file_io, shell, browser, 等)
  │       ├── 检查 agent_config.tools.builtin_tools 配置
  │       │   └── 根据 enabled=True/False 决定每个工具是否注册
  │       ├── 设置 async_execution 标志 (shell / delegate 支持后台异步)
  │       ├── 注册 MCP 客户端提供的工具
  │       └── 如果 coding_mode 启用: 注册 LSP + AST 工具
  │
  ├── 4. 注册技能
  │   └── _register_skills(toolkit, effective_skills)
  │       └── 对每个技能: toolkit.register_agent_skill(skill_dir)
  │           → 读取 SKILL.md, 将技能描述注入 system prompt
  │
  ├── 5. 构建系统提示词
  │   └── _build_sys_prompt()
  │       ├── PromptBuilder(working_dir, heartbeat_enabled, language, memory_manager)
  │       │   ├── 加载 AGENTS.md → Agent 身份描述
  │       │   ├── 加载 SOUL.md   → Agent 性格/行为准则
  │       │   └── 加载 PROFILE.md → 用户偏好/背景信息
  │       ├── 附加多模态能力提示 (model reject 媒体块提示)
  │       ├── 附加环境上下文 (_env_context)
  │       └── 如果 coding_mode 启用: 附加编码模式提示词模板
  │
  ├── 6. 创建模型和格式化器
  │   └── create_model_and_formatter(agent_id)
  │       ├── 查询 agent 配置中的 active_model
  │       ├── 从 providers 管理器获取对应的模型类
  │       └── 封装 FileBlockSupportFormatter (支持文件/媒体块)
  │
  ├── 7. 初始化 ReActAgent 父类
  │   └── super().__init__(name, model, sys_prompt, toolkit, memory=InMemoryMemory(),
  │       formatter, max_iters, plan_notebook)
  │
  ├── 8. 注册记忆系统工具
  │   └── memory_manager.list_memory_tools() → 注册到 toolkit
  │       (如: 检索记忆、存储记忆等工具函数)
  │
  ├── 9. 配置上下文管理器
  │   └── context_manager.get_agent_context() → 替换 self.memory 为 AgentContext
  │       (AgentContext 包装了记忆操作,支持智能截断)
  │
  ├── 10. 初始化命令处理器
  │    └── CommandHandler(agent_name, memory, memory_manager, context_manager)
  │
  └── 11. 注册钩子
       └── _register_hooks()
           ├── BootstrapHook → pre_reasoning 钩子 (首次交互引导)
           └── 上下文管理器钩子 → pre_reply / pre_reasoning / post_acting / post_reply

三、ReAct 循环机制

这是 Agent 的核心------它决定了 Agent 如何理解用户问题、决定需要做什么、执行工具、再从结果中继续推理,最终给出完整答案。这个循环本质上模拟了人类解决问题的思考过程:先想(Reasoning),再做(Acting),观察结果,继续想,直到问题解决。

AgentRunner 调用 agent.reply(msg) 时,整个生命周期如下展开。这里有一个容易被忽略的设计细节:系统命令的检查发生在 ReAct 循环之外 。这意味着 /compact/new 等命令可以瞬间执行,不需要经过 LLM 推理,既快又不消耗 Token。这也是为什么命令响应比普通对话快得多的原因。

erlang 复制代码
reply(msg)
  │
  ├── 1. 设置线程本地上下文 (workspace_dir, session_id, timeout 等)
  │
  ├── 2. 处理文件/媒体块
  │   └── process_file_and_media_blocks_in_message(msg)
  │       → 将文件路径转为文件内容/媒体 URI
  │
  ├── 3. 检查系统命令
  │   └── command_handler.is_command(query)
  │       ├── "/compact"   → 压缩记忆
  │       ├── "/new"       → 开始新对话
  │       ├── "/clear"     → 清除记忆
  │       ├── "/history"   → 查看历史
  │       └── "/plan ..."  → 启动计划模式
  │       ← 是命令 → 直接处理并返回,不走 ReAct
  │
  └── 4. 进入 ReAct 循环 (super().reply())
       │
       ├── ▶ 循环开始
       │   │
       │   ├── (A) 钩子: pre_reasoning
       │   │   ├── BootstrapHook → 首次交互引导
       │   │   └── ContextManager.pre_reasoning → 上下文截断
       │   │
       │   ├── (B) _reasoning(tool_choice)
       │   │   调用 LLM 模型推理
       │   │   ├── 主动层: 若模型不支持多模态,提前剥离媒体块
       │   │   ├── 调用 LLM → 返回 assistant_msg
       │   │   └── 被动层: 若 LLM 报错(媒体错误),剥离媒体块重试
       │   │   └── 自动继续: 若 assistant 只返回文字(没调工具),可自动再调一次
       │   │
       │   ├── (C) 检查是否有 tool_call
       │   │   ├── 没有 → 结束循环,返回最终回答
       │   │   └── 有 tool_call → 进入工具执行
       │   │
       │   ├── (D) _acting(tool_call)  ← 关键拦截点
       │   │   ├── [QwenPawAgent._acting]
       │   │   │   ├── 检查 plan 门控 (计划突变后限制工具)
       │   │   │   └── 修复 JSON 参数 (某些模型返回 stringified JSON)
       │   │   │
       │   │   ├── [ToolGuardMixin._acting] ← 安全管理器
       │   │   │   ├── 获取执行级别 (OFF/AUTO/SMART/STRICT)
       │   │   │   ├── 检查 denied_tools → 命中则自动拒绝
       │   │   │   ├── 运行守卫引擎 (所有已注册的 guardian)
       │   │   │   │   ├── file_guardian → 检查文件路径
       │   │   │   │   ├── shell_evasion_guardian → 检查命令注入
       │   │   │   │   └── approval_guardian → 需要人工审批
       │   │   │   ├── 根据结果决定: 自动允许 / 自动拒绝 / 需要审批
       │   │   │   └── 需要审批时 → 等待用户确认 (前端弹窗/渠道交互)
       │   │   │
       │   │   └── [ReActAgent._acting] ← 实际工具执行
       │   │       └── 执行工具函数 → 返回 tool_result
       │   │
       │   ├── (E) 钩子: post_acting
       │   │   └── ContextManager.post_acting → 处理工具结果(截断等)
       │   │
       │   └── (F) 回到循环开始 (最多 max_iters 次)
       │
       ├── (G) _summarizing() (可选)
       │   若需要总结 (达到 compact 阈值) → 压缩历史
       │
       ├── (H) 钩子: post_reply
       │   └── ContextManager.post_reply
       │
       └── (I) 返回最终回复 Msg

关键点:_acting 调用链的依赖顺序

QwenPawAgent._acting()ToolGuardMixin._acting()ReActAgent._acting()

每个 _acting 方法内部都调用了 super()._acting(tool_call),通过 Python 的 MRO (C3 线性化) 形成调用链。这是 Mixin 模式的核心优势------透明拦截。

scss 复制代码
QwenPawAgent._acting(tool_call)
  ├── Plan gate check
  └── super()._acting() → ToolGuardMixin._acting()
       ├── Safety check (denied/guarded/approval)
       └── super()._acting() → ReActAgent._acting()
            └── === 真实工具执行 ===

四、工具系统详解

工具系统是 Agent 与外界交互的唯一通道。LLM 本身是一个"纯文本大脑"------它无法直接读取文件、执行命令或浏览网页。工具系统扮演了"手和眼"的角色:LLM 决定"做什么",工具系统负责"怎么做"。因此,工具系统的设计直接决定了 Agent 的能力边界和安全性。

4.1 工具注册流程

_create_toolkit() 是工具注册的核心方法,它在初始化阶段被调用一次,完成所有工具的定义和注册。以下流程图展示的每个工具注册步骤都对应一个副作用------在 System Prompt 中追加该工具的 JSON Schema 描述。这意味着:注册的工具越多,System Prompt 越长,LLM 的选择空间也越大,但同时 Token 消耗也越高 。这就是为什么 builtin_tools 配置提供了按需禁用特定工具的能力------这是一个 Token 成本与能力覆盖范围之间的权衡。

scss 复制代码
QwenPawAgent._create_toolkit()
  │
  ├── 创建 Toolkit 实例
  │
  ├── 循环注册内置工具 (每个工具一个 register_tool_function 调用)
  │   ├── execute_shell_command    (async_execution=可选)
  │   ├── execute_python_code
  │   ├── read_file / write_file / edit_file / append_file
  │   ├── grep_search / glob_search
  │   ├── browser_use              (async_execution=True)
  │   ├── desktop_screenshot
  │   ├── view_image / view_video
  │   ├── get_current_time / set_user_timezone
  │   ├── send_file_to_user
  │   ├── list_agents / chat_with_agent / submit_to_agent / check_agent_task
  │   ├── delegate_external_agent   (async_execution=可选)
  │   └── materialize_skill         (始终启用)
  │
  ├── 根据 agent_config.tools.builtin_tools 过滤
  │   └── 检查每个工具的 enabled 标志
  │       └── disabled → 从 toolkit 中移除 (unregister)
  │
  ├── 注册 MCP 工具
  │   └── 遍历 _mcp_clients, 将每个 MCP client 的 tools 注册进来
  │
  └── [CodingModeMixin] 注册额外工具
      └── 若 coding_mode 启用:
          ├── make_lsp_tool(available_languages) → LSP 语言服务工具
          └── ast_search() → AST 语法树搜索工具

4.2 namesake_strategy(工具重名处理)

当不同来源的工具出现同名时,需要一个策略来决定谁"赢"。这个问题的典型场景是:MCP 客户端注册了一个叫 read_file 的工具,而内置工具也有 read_file------二者同名工具名冲突在多来源工具注册的场景下几乎是必然发生的。

策略 行为 使用场景
"skip" (默认) 跳过后续注册的同名工具,保留最先注册的 保守策略,内置工具优先
"override" 最新注册的覆盖旧的 MCP 工具优先于内置
"raise" 抛出异常 研发环境,发现重复立刻暴露问题
"rename" 自动重命名(加 _1, _2 后缀) 两者都想保留,让 LLM 自行选择

4.3 工具执行安全守卫 (ToolGuardMixin)

安全守卫是 Agent 安全体系的核心防线。它的设计思路是:LLM 可能被 prompt injection 攻击骗去执行危险操作,所以不能信任 LLM 发出的任何工具调用,必须在执行前逐一检查。这里的核心矛盾是安全与效率------每次工具调用都要过一遍守卫检查,如果每个检查都让用户确认,用起来会非常烦琐。解决方案是引入"执行级别"概念:用户可以根据场景选择宽松(AUTO)或严格(STRICT)模式。

scss 复制代码
工具执行级别
├── OFF     → 完全绕过守卫(不推荐,仅用于调试)
├── AUTO    → 正常检查,自动拒绝高危,自动允许低风险
├── SMART   → 类似 AUTO,但自动允许 INFO/LOW 风险的发现(比 AUTO 更聪明地放行)
└── STRICT  → 所有工具都需要人工审批(最安全但最慢模式)
     (由 agent_config.security.tool_guard.execution_level 控制)

守卫检查流程:
1. 获取 tool_name + tool_input
2. 检查 denied_tools 黑名单 (自动拒绝 ------ 可配置哪些工具永远不准用)
3. 检查是否 guarded 工具 (需额外检查 ------ 只对"有风险"的工具运行守卫)
4. 运行守卫 (guardians):
   ├── file_guardian → 检查文件路径是否越界 (防止读取 /etc/passwd 等)
   ├── shell_evasion_guardian → 检查命令注入 (防止 rm -rf / 等)
   ├── approval_guardian → 是否需要审批 (根据 severity 决定)
   └── always_run 守卫 → 对非 guarded 工具也运行 (兜底检查)
5. 根据 GuardResult 的 severity (严重程度) 决定:
   ├── auto_denied  → 自动拒绝 + 返回拒绝消息
   ├── needs_approval → 发送审批请求 (等待用户确认)
   └── auto_allowed → 直接执行

守卫系统的设计借鉴了责任链模式:多个 Guardian 顺序执行,每个 Guardian 独立检查自己的关注点,任何一个 Guardian 返回拒绝,整个调用就被阻断。这种设计让新增安全检查变得非常容易------只要写一个新的 Guardian 类注册进去即可,不需要修改既有代码。


五、提示词系统

提示词系统负责将 Agent 的"人格"和行为准则注入到 System Prompt 中。它的设计亮点是 "模板 + 占位符"模式------AGENTS.md、SOUL.mdPROFILE.md 这些文件是"人的描述文件",由非技术用户编写,而 PromptBuilder 负责在运行时将它们动态拼装成 LLM 可理解的指令。

这种设计带来的灵活性是巨大的:改变 Agent 的行为不再需要改代码,只需要修改 AGENTS.md 中的描述文本。不同的用户可以有各自的 AGENTS.md,同一份代码库可以衍生出不同"性格"的 Agent。

5.1 提示词构成

最终的 sys_prompt 由以下部分拼接而成:

ini 复制代码
[来自 PromptBuilder]
# AGENTS.md         ← Agent 身份描述 (你是谁、你的能力)
                    (可选包含 heartbeat 心跳节和 memory 记忆节)
                    
# SOUL.md           ← Agent 性格/行为准则

# PROFILE.md        ← 用户的偏好/背景信息

[来自编码模式]
## Coding Mode      ← 编码工作流指南 (仅当 coding_mode 启用)

[来自多模态检测]
<multimodal hint>   ← 若模型不支持多模态,提示 Agent 告知用户

[来自环境上下文]
<env_context>       ← 当前工作目录、系统信息等

5.2 PromptBuilder 加载逻辑

python 复制代码
DEFAULT_FILES = ["AGENTS.md", "SOUL.md", "PROFILE.md"]
  • 加载顺序固定:AGENTS.mdSOUL.mdPROFILE.md。这个顺序是有意设计的:先定义"你是谁"(AGENTS.md),再定义"你的性格"(SOUL.md),最后告诉 Agent"用户是谁"(PROFILE.md)。这样 Agent 在阅读提示词时,先建立自我认知,再了解行为准则,最后理解服务对象的背景------符合人类的认知逻辑。
  • 所有文件可选,不存在则跳过。这使得 Agent 的"人格配置"是渐进增强的:只有一个 AGENTS.md 也能工作,三个文件都配齐则效果最佳。
  • 每个文件前自动加 # 文件名 作为节标题,在 System Prompt 中形成清晰的文档结构。
  • AGENTS.md 支持特殊标记:
    • <!-- heartbeat:start --> ... <!-- heartbeat:end --> --- 心跳节,可根据开关启用/禁用。心跳节是定期向用户推送状态信息的机制,类似于 IDE 的状态栏通知,但以自然语言呈现。
    • <!-- memory:start --> ... <!-- memory:end --> --- 记忆节,会被移除并替换为 memory_manager 生成的记忆提示。这个占位符机制让 AGENTS.md 的作者不需要关心记忆系统怎么工作------他们只需要声明"这里放记忆",剩下的由代码处理。

5.3 记忆提示注入

PromptBuilder._process_memory_section():

  1. 删除 AGENTS.md 中的 <!-- memory:start -->...<!-- memory:end --> 区域
  2. 调用 self.memory_manager.get_memory_prompt(language) 生成记忆提示
  3. AGENTS.md 末尾追加该记忆提示

这个流程的核心意图是分离关注点AGENTS.md 的作者只需要在模板中留一个 <!-- memory:start --> 标记,运行时由记忆系统自动填充真实的记忆内容。这意味着记忆系统可以独立演进------即使底层从向量数据库切换到了关系数据库,AGENTS.md 也不需要修改。


六、命令处理器 (CommandHandler)

命令处理器是 Agent 与用户之间的一条"快捷键"通道。它的设计意图是:某些操作(清空对话、压缩记忆、查看历史等)如果也要走 LLM 推理,不仅速度慢,而且浪费 Token------Agent 明明知道 /new 是什么意思,却还要让 LLM 在上下文中"理解"一遍。解决方案是在 reply() 的入口处、ReAct 循环启动之前,拦截所有以 / 开头的输入,由 CommandHandler 直接处理。

这种设计让命令响应做到毫秒级------不经过 LLM 推理,不消耗 Token,不经过工具调用的安全检查链。这也是为什么 /compact 能瞬间压缩数百条对话历史,而如果让 LLM 来做可能需要几秒钟。

用户可以通过 / 开头的命令控制 Agent 行为:

命令 功能 实现
/compact 压缩记忆 (自动摘要 + 清理) 调用 memory_manager.summarize_when_compact()
/new 清空当前会话记忆 调用 memory.clear()
/clear 同上 (别名) 同上
/history 显示对话历史 遍历 memory 输出
/compact_str 显示压缩统计 调用 memory_manager.list_summarize_status()
/summarize_status 显示摘要任务状态 同上
/message <id> 查看某条消息详情 按 ID 查找记忆
/dump_history 导出历史为 JSON 输出到文件
/load_history 从 JSON 加载历史 从文件读取
/proactive 触发主动回忆 调用 memory_manager 的 proactive 方法
/plan 计划模式状态 (不带参数时) 查看计划系统状态
/plan <desc> 创建新计划 (带参数) 传递给 _maybe_inject_skill → 触发计划技能

命令在 reply() 入口处拦截,不走 ReAct 循环。


七、钩子系统 (Hooks)

钩子系统是 AgentScope 框架提供的一种事件回调机制,类似于 Web 框架中的中间件。它的设计目的是:在不修改 Agent 核心循环代码的前提下,在特定生命周期点"插入"自定义行为。Agent 的钩子分散在 ReAct 循环的多个位置,每个钩子只负责一件小事------合起来构成了完整的请求处理管线。

Agent 使用 AgentScope 的钩子机制,在 ReAct 循环的关键生命周期点注入行为:

钩子类型 注册位置 作用
pre_reply ContextManager 回复前准备 (上下文加载、状态检查)
pre_reasoning BootstrapHook 首次交互引导
pre_reasoning ContextManager 推理前上下文截断
post_acting ContextManager 工具执行后处理
post_reply ContextManager 回复后清理/持久化

BootstrapHook

BootstrapHook 是钩子系统中一个具体的实现,它的设计意图是解决 Agent"冷启动"问题------第一次对话时,Agent 对用户一无所知,也没有历史上下文可以参考。BootstrapHook 通过读取 BOOTSTRAP.md 引导文件,为用户的第一条消息补充背景信息,帮助 Agent 快速进入状态。

首次对话时,如果工作目录存在 BOOTSTRAP.md

  1. 读取引导内容
  2. 将其 prepend 到第一条用户消息前
  3. 写入 .bootstrap_completed 标记文件,防止重复触发

BOOTSTRAP.md 的一个典型用法是:在 Agent 启动时告诉它"当前项目是什么、最新进展到哪一步了、用户可能想问什么问题",这样用户的第一个问题就能得到准确的回答,而不是让 Agent 先去猜项目背景。

注意 BootstrapHook 只触发一次(由标记文件控制),之后的对话不再重复注入。这保证了引导信息不会随着对话进行而不断膨胀,浪费 Token。


八、MCP (Model Context Protocol) 集成

MCP 是 Agent 与外部的 AI 工具生态对接的标准协议。可以把它理解为 AI 世界的"USB 接口"------通过标准化的协议,任何实现了 MCP 的服务都可以像即插即用的外设一样接入 Agent,提供图片生成、数据查询、云端 API 调用等能力。

register_mcp_clients(mcp_clients) 方法支持将外部 MCP 工具集成到 Agent 中。这里的关键挑战是连接可靠性:MCP 客户端可能因为网络波动或服务重启而断开连接,因此实现中包含了状态恢复和定时重连机制。

register_mcp_clients(mcp_clients) 方法支持将外部 MCP 工具集成到 Agent 中:

erlang 复制代码
register_mcp_clients(clients)
  │
  ├── 遍历 MCP 客户端列表
  │
  ├── 对每个客户端:
  │   ├── 尝试连接/恢复 (HttpStatefulClient / StdIOStatefulClient)
  │   ├── 获取客户端提供的 tools 列表
  │   └── 注册到 toolkit (prefix=MCP 工具名 + 客户端名称)
  │
  └── 如果连接失败:
      ├── 记录错误
      └── 定时重连 (根据错误类型决定重试策略)

MCP 客户端可以在 Agent 运行时热加载/热更新,支持添加、移除、刷新工具列表。


九、多模态处理机制

多模态处理机制解决的核心问题是:不同 LLM 对多模态(图片、视频、音频)的支持能力差异很大。通义千问 VL 系列和 GPT-4V 可以直接理解图片,而一些纯文本模型(如 DeepSeek-V2)碰到图片会报错。如果 Agent 不做处理,用户发了一张图片,纯文本模型直接崩溃------这对用户体验是灾难性的。

这是 _reasoning 重写的核心改进点,体现了两层防御设计:

scss 复制代码
_reasoning(tool_choice)
  │
  ├── 1. 主动过滤层 (proactive)
  │   ├── 检查模型是否支持多模态 (supports_multimodal 缓存)
  │   ├── 检查之前是否 reject_media (capability cache)
  │   └── 若不支持:
  │       ├── 方法 A: request-time 剥离 (通过 formatter 在格式化时剥离)
  │       └── 方法 B: memory-time 剥离 (直接修改 memory 中的媒体块)
  │
  ├── 2. 调用 LLM
  │   └── super()._reasoning(tool_choice)
  │
  └── 3. 被动过滤层 (passive fallback)
      └── 如果 LLM 抛出 bad_request / media error:
          ├── 记录到 capability cache (rejects_media=True)
          ├── 剥离剩余媒体块
          └── 重试 _reasoning

为什么要两层?

  • 主动层:避免 API 调用失败,节省 Token。如果上一个请求已经发现模型拒绝媒体,就在 capability cache 中标记,后续直接剥离,不会再白花一次 API 调用的钱。
  • 被动层:处理主动层没覆盖到的边缘情况(如图片在 tool_result 中)。有些模型的媒体块在用户消息层能被主动过滤,但工具执行返回的图片结果在另一种上下文中,可能仍需被动兜底。

这种两层防御设计的最终效果是:用户无论用什么模型,Agent 都不会因为媒体块而中断工作------最多是告诉用户"我看不到图片,但你可以描述一下"。


十、关键设计模式总结

整个 Agent 系统在多个层面使用了经典的设计模式,每个模式的选择都有其特定的工程考量。下表归纳了各模式的使用位置及设计意图:

模式 使用位置 说明
Mixin (多重继承) QwenPawAgent(CodingModeMixin, ToolGuardMixin, ReActAgent) 透明拦截 _acting 方法链
策略模式 记忆系统 BaseMemoryManager 接口,多个后端实现
工厂方法 create_model_and_formatter() 根据配置创建模型和格式化器
钩子模式 AgentScope hook 系统 生命周期回调 (pre_reply, pre_reasoning, post_acting...)
注册表模式 记忆管理器、工具注册 memory_registry 字典,按名称查找实现类
建造者模式 PromptBuilder 逐步构建系统提示词
命令模式 CommandHandler 系统命令解析和分发
责任链模式 ToolGuard guardians 多个守卫顺序检查,任一可阻断
代理模式 ToolGuardMixin._acting 在真实工具执行前插入安全检查
模板方法模式 ReActAgent.reply()_reasoning()_acting()_summarizing() 固定流程,子类重写特定步骤

这里特别值得关注的是 Mixin 模式的运用 :大部分 Python 项目使用 Mixin 只是为了"复用代码",但这里的 ToolGuardMixinCodingModeMixin 复用的是控制流 而非数据或方法。它们通过 super()._acting() 形成调用链,每个 Mixin 在链中扮演一个中间件角色。这种用法在 Python 生态中并不常见,但恰恰是处理"对一个核心方法的横切关注点"最简洁的表达------比装饰器更灵活(装饰器难以形成链式上下文),比组合更透明(组合需要外部包装类显式转发)。

另一个有趣的细节是 注册表模式在记忆系统中的变体memory_manager 内部维护了一个 memory_registry 字典,键是记忆类型名称,值是具体的记忆类。当配置指定使用某种记忆时,不是通过 if-else 分支来选择,而是直接从注册表中按名称查找并实例化。这使得新增记忆后端不需要修改任何分支逻辑------只需要在注册表中注册一个新的类即可。


十一、数据流总图 (完整生命周期)

yaml 复制代码
AgentRunner.run_query()
  │
  ├── 加载会话状态 (SafeJSONSession.load_session_state)
  │
  ├── 创建/复用 QwenPawAgent
  │   ├── __init__: 工具注册、技能加载、提示词构建、模型创建、钩子注册
  │   │
  │   └── 如果复用: 更新 request_context, 重建 system prompt
  │
  ├── agent.reply(msg)
  │   │
  │   ├── (pre_reply 钩子) → ContextManager: 加载上下文
  │   │
  │   ├── 检查系统命令 → 若是命令则直接返回
  │   │
  │   ├── ▸▸▸ ReAct 循环开始 ▸▸▸
  │   │   │
  │   │   ├── (pre_reasoning 钩子) → Bootstrap + ContextManager
  │   │   │
  │   │   ├── _reasoning() → 调用 LLM
  │   │   │   ├── 多模态过滤 (主动+被动)
  │   │   │   └── 返回 assistant_msg (可能含 tool_calls)
  │   │   │
  │   │   ├── 检查 tool_calls
  │   │   │   ├── 无 → break, 返回 assistant_msg
  │   │   │   └── 有 → 进入工具执行
  │   │   │
  │   │   ├── _acting(tool_call)  ← 三层拦截
  │   │   │   ├── QwenPawAgent: Plan gate + JSON 修复
  │   │   │   ├── ToolGuardMixin: 安全检测 + 审批流程
  │   │   │   └── ReActAgent:  实际调用工具函数
  │   │   │
  │   │   ├── (post_acting 钩子) → ContextManager: 处理结果
  │   │   │
  │   │   └── 回到循环开始 (最多 max_iters 次)
  │   │
  │   ├── _summarizing() → 若需 compact,自动摘要
  │   │
  │   └── (post_reply 钩子) → ContextManager: 清理
  │
  ├── 保存会话状态 (SafeJSONSession.save_session_state)
  │
  └── 返回响应 → Channel → 用户

十二、二次开发最常见的切入点

如果你需要在自己的项目中使用或扩展 Agent,以下是最常用的开发入口和对应的使用场景:

需求 切入点 说明
修改 Agent 行为 编辑 AGENTS.md / SOUL.md / PROFILE.md 无需改代码,只改描述文本
添加自定义工具 编写工具函数 → 在 _create_toolkit() 中注册 工具函数需符合 AgentScope 的 tool 装饰器格式
添加安全检查 实现新的 Guardian 类 → 注册到 ToolGuardMixin 继承 BaseGuardian,实现 check(tool_name, tool_input) 方法
自定义记忆后端 实现 BaseMemoryManager → 注册到 memory_registry 只需要实现检索、存储、摘要等核心方法
新增系统命令 CommandHandler 中添加命令处理函数 注册命令名 → 处理函数映射即可
添加钩子 实现 AgentScope 的 Hook 基类 → 在 _register_hooks() 中挂载 可在 pre/post 各阶段插入行为
修改 ReAct 循环 重写 QwenPawAgent._reasoning()_acting() 注意调用链中的 super() 位置
集成外部服务 实现 MCP 客户端 → 通过 register_mcp_clients() 注册 标准 MCP 协议,即插即用

每个切入点的修改都是局部且有边界的:改 AGENTS.md 不会影响安全系统,加 Guardian 不会影响 ReAct 循环,注册新工具不会影响其他工具。这种低耦合设计是 Agent 架构的核心工程目标------让开发者每次只需要关注一个模块,不必担心改动波及到不相关的部分。

相关推荐
百珏2 小时前
个人理解的AI Code Review 架构的三代演进
架构·aigc·ai编程
Sincerelyplz2 小时前
【AI会议纪要实践】mapReduce、RAG 与结构化输出
java·后端·agent
Ailrid2 小时前
设计模式——行为型设计模式:阅读笔记与个人思考
架构
贺国亚2 小时前
Agent 框架 · LangChain / LangGraph / AutoGen / CrewAI
面试
Ailrid2 小时前
设计模式——论UI中的组合与OOP
架构
zavoryn2 小时前
后端接入 AI Agent:Tool Calling 网关、幂等与审计日志实战
后端·架构
冰雪情缘long2 小时前
Android架构分层+架构模式+设计模式的关系理解
架构
青山师2 小时前
动态规划算法深度解析:从状态转移方程到工业级优化
数据结构·算法·面试·动态规划·代理模式·java面试
zhangjw342 小时前
第15篇:Java多线程零基础入门,进程线程、线程创建方式、线程生命周期、线程安全彻底吃透
java·开发语言·面试