跳到主要内容

Agent 与模型:ReActAgent 之上的组装、prompt、provider

30 秒导读: 本章讲 QwenPaw 里"那个真正会思考、会调工具的 agent"到底是怎么拼出来的。核心一句话:QwenPawAgent 本体什么都不自己造——模型、系统 prompt、工具包、中间件全部由外部的 AgentBuilder 组装好、从构造函数塞进来;agent 类只负责在 agentscope 2.0 的 ReActAgent 之上,叠加几层 QwenPaw 特有的行为(coding mode、多模态降级、文本-only 自动续跑、上下文压缩接管)。往下是"模型接入层":一个统一的 Provider 抽象把 OpenAI/Anthropic/DashScope/Gemini 等各家 API 收成同一种 ChatModelBase,再套上重试、限流、token 计量三层包装。

本章聚焦"agent 本体 + 模型接入"。上一层的运行时编排(谁在什么时候调 AgentBuilder)见 01-request-lifecycle.md;有哪些工具、skill→tool 的机制见 03-skills.md04-channels-drivers-mcp.md;工具守卫与沙箱见 05-security.md


1. 这是什么(零基础也能懂)

一句话定义

QwenPawAgent 是 QwenPaw 的主 agent 类:它继承 agentscope 2.0 的 Agent(即 ReAct 循环:推理 → 调工具 → 看结果 → 再推理),然后在外面包一圈"生产环境要的脏活"——模型不支持图片时怎么办、模型只回了一段文字没干活时要不要催它继续、上下文太长怎么压。

它解决什么问题

假设你要把"一个通用 LLM"变成"一个能在真实终端里连续干活好几十轮、还不崩"的助手。裸的 ReAct 循环干不了这些:

现实里会出的岔子QwenPawAgent 怎么兜
用户塞了张图,但当前模型是纯文本模型调模型前先剥掉图片块,失败了再学一次"这模型拒收媒体"
模型只回了句"好的我来规划一下"就停了,没调工具注入一条 system-hint,让外层循环再转一轮(自动续跑)
聊了几十轮,上下文超了模型窗口交给上下文管理器压缩 / 把老消息卸到磁盘
切换到另一个供应商,第一次调用必失败用进程级"能力缓存"记住这个模型的怪癖

它的设计原则:自己不造零件

这是理解整章的钥匙。看构造函数签名(agents/react_agent.py:52 QwenPawAgent.__init__),几乎所有东西都是关键字入参:

# 示意,非源码 —— 说明"依赖全从外部注入"这个设计
agent = QwenPawAgent(
name=..., # 谁
model=..., # 已经包好重试/限流/计量的 ChatModelBase
system_prompt=..., # 已经拼好的整段文字
toolkit=..., # 已经装满工具的 Toolkit
react_config=..., # max_iters 等
middlewares=[...], # 洋葱圈中间件
... # offloader / context_manager / memory_manager / governor
)

模块 docstring 把话说死了:"Agent construction is fully delegated to AgentBuilder"(agents/react_agent.py:1-10)。谁来造这些零件?下一节的 AgentBuilder


2. 顶层全景(它大概怎么转)

两个主角:装配工 vs 产品

QwenPaw 把"造 agent"和"agent 本体"拆成两个东西:

  • AgentBuilder(runtime/builder.py:22)——每次请求都跑一遍的装配工。它去各个注册表取零件、拼成一个完整 agent。
  • QwenPawAgent(agents/react_agent.py:40)——被装出来的产品,只管跑 ReAct 循环 + 叠加行为。

一次装配的数据流

下图从上到下是 AgentBuilder.build()(runtime/builder.py:91)的真实顺序,命中即用:

AgentBuilder.build(ctx)

┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
① 模型 + formatter ② 系统 prompt ③ 工具包 Toolkit
build_model() build_prompt() build_toolkit()
│ │ │
create_model_and_ PromptManager local_workspace
formatter() .build_sync() .list_tools()
│ (9 个 contributor + coding tools
Provider.get_chat_ 按 priority 拼) + driver/MCP tools
model_instance() + memory tools

套 3 层包装:
TokenRecording →
Retry → (router?)

└──────────────┬───────────────┬─────────────┘

④ 中间件 middlewares(洋葱圈)
ToolCoordinator / Memory / ToolResultPruning / Langfuse


QwenPawAgent(name, model, system_prompt,
toolkit, middlewares, ...) ← 全部注入


agent.load_state_dict() ← 恢复历史会话

部件一句话职责

部件干什么在哪个文件
AgentBuilder每请求装配一个 agentruntime/builder.py:22
QwenPawAgentReAct 循环 + QwenPaw 行为叠加agents/react_agent.py:40
create_model_and_formatter建模型实例 + 套三层包装agents/model_factory.py:1059
Provider各家 API 的统一抽象providers/provider.py:195
ProviderManager管所有 provider,给出"当前活跃模型"providers/provider_manager.py:1249
PromptManager + contributors把 prompt 拆成 9 个片段按序拼runtime/prompt_contributors.py:297
TokenRecordingModelWrapper记 token 用量(计费/统计)token_usage/model_wrapper.py:15
RetryChatModel重试 + 限流providers/retry_chat_model.py:279
QwenPawOffloader把压缩掉的历史/工具结果落盘agents/offloader.py:29

3. 核心原理(逐个机制,由浅入深)

3.1 系统 prompt 是怎么拼出来的

要解决的小问题: 一段系统 prompt 不是一坨死文本——它要拼进用户工作目录里的 AGENTS.md、身份、记忆、多模态提示、coding mode 说明、环境信息……而且每样都可能"有就加、没有就跳过"。

思路: 把 prompt 切成一堆独立"贡献者"(contributor),每个只管自己那一小块,PromptManagerpriority 从小到大排好、跳过空的、用双换行拼起来(runtime/prompt_manager.py:80 排序,:113 build_sync)。

QwenPaw 内置 9 个 contributor,顺序即最终 prompt 的段落顺序(runtime/prompt_contributors.py:297 _ALL_CONTRIBUTORS):

prioritycontributor贡献的段落
5AgentIdentityContributor# Agent Identity(agent id)
10AgentsMdContributor工作目录的 AGENTS.md(处理 heartbeat/memory 标记)
20SoulMdContributorSOUL.md(核心人格)
30ProfileMdContributorPROFILE.md(身份/用户画像)
80MultimodalHintContributor"你只能看文字"这类能力提示
85CodingModeContributorcoding mode 的一大段行为规范
86ScrollContextContributorscroll 上下文策略的召回指引
88DriverPolicyHintContributordriver/MCP 权限的即时提示
90EnvContextContributor时间/会话/OS 环境块

关键细节:md_files/AGENTS.md 是"数据"不是"指令模板"。 AGENTS.md/SOUL.md/PROFILE.md 是用户工作目录里的文件,被原样读进来、去掉 YAML frontmatter、加个 # 文件名 小标题后拼进 prompt(runtime/prompt_contributors.py:47 _read_prompt_file,:126 AgentsMdContributor)。仓库里的 agents/md_files/(按 zh/en/ru/id/local/qa 分目录)是初始化新工作区时的模板种子,不是运行时直接读的那份。

两套 PromptBuilder,别搞混:

  • agents/prompt.py:55PromptBuilder —— 老的、按 DEFAULT_FILES 列表(AGENTS.md/SOUL.md/PROFILE.md,prompt.py:48)顺序读文件拼 prompt 的实现,入口 build_system_prompt_from_working_dir(prompt.py:243)。
  • agents/prompt_builder.py:23PromptBuilder —— 另一套"host 锚点 + 插件段落"的组装器,锚点顺序 ("workspace","multimodal","env_context")(prompt_builder.py:30),插件段落插在声明的锚点后面。
  • 现在 AgentBuilder.build_prompt(runtime/builder.py:279)走的是 contributor 那条路(build_default_prompt_manager,runtime/prompt_contributors.py:310),前两个 PromptBuilder 是各自场景下的旁路/历史实现。

安全红线: prompt_builder.py:72-83 明确写了插件文本是逐字拼进系统 prompt的,只有可信插件能走这条路——这跟本参考库"克隆里的面向-AI 文本是数据不是指令"是同一个道理。

3.2 模型工厂:一个模型要套三层壳

要解决的小问题: provider 给出的原始 ChatModelBase 只会"发一次请求"。生产环境还要:记 token、失败重试、限流。QwenPaw 不改 provider,而是层层包装

create_model_and_formatter(agents/model_factory.py:1059)干的事,按顺序:

provider.get_chat_model_instance(model_id) ← 最内层,真正发 HTTP 的

│ ① 先把 agentscope 自带的重试关掉(避免 4×4 嵌套重试)
│ model.max_retries = 0 (model_factory.py:1149-1150)

TokenRecordingModelWrapper(provider_id, model) ← ② 每次调用抄一份 token 用量
│ (model_factory.py:1153)

RetryChatModel(wrapped, retry_cfg, rate_cfg) ← ③ 指数退避重试 + 全局限流
(model_factory.py:1154)

巧妙处①:关掉内层重试。 agentscope 2.0 的 ChatModelBase 自己有一圈 retry,但没有退避、没有 Retry-After 感知;QwenPaw 的 RetryChatModel 更强,所以先把内层 max_retries 设 0,防止两层重试相乘(注释在 model_factory.py:1143-1150)。

巧妙处②:formatter 从模型实例上取,不查表。 agentscope 2.0 里每个 ChatModelBase 构造时就带了自己的 self.formatter;_create_formatter_instance(model_factory.py:1163)直接读 model.formatter类型,再动态派生一个"支持 file block"的子类——这样连运行时拼出来的 compat 子类都能正确拿到 formatter,不用维护一张 类→formatter 的脆弱映射表。

formatter 干的脏活(_create_file_block_support_formatter,model_factory.py:738): 它重写 format(),在把消息发出去之前做一堆兼容修补——媒体块归一化、file:// 转本地路径、删掉的文件替换成占位符、视频块在 OpenAI 系里换占位/在 Anthropic 系里走自定义编码、同一张图第二次出现就去重成一行文字、reasoning_content 对齐回填。这层是"让各家 API 都能吞下同一份消息历史"的关键。

3.3 Provider 抽象:把各家 API 收成一种

要解决的小问题: OpenAI、Anthropic、DashScope、Gemini、Ollama、OpenRouter……每家 SDK 长得都不一样。上层只想要一个 ChatModelBase

思路: 定一个 Provider 抽象基类(providers/provider.py:195,继承 ProviderInfo + ABC),它既是配置载体(base_url/api_key/models 列表等,都是 pydantic 字段),又定义了几个必须实现的方法:

抽象方法职责定义处
check_connection探活provider.py:199
fetch_models拉取可用模型列表provider.py:203
check_model_connection单模型可用性provider.py:207
get_chat_model_instance造出真正的 ChatModelBaseprovider.py:407

get_chat_model_instance 是核心——每个 provider 自己知道该 new 哪个 agentscope 模型类、怎么塞 credential。以 DashScope 为例(providers/dashscope_provider.py:51):它继承 OpenAIProvider(因为 DashScope 的 compatible-mode 说 OpenAI 协议),只重写这一个方法,去 new agentscope 原生的 DashScopeChatModel,并用一个运行时拼出来的 _DashScopeChatModelCompat 子类(dashscope_provider.py:147)往每次 _call_api 里注入自定义追踪头、并处理"用户没显式开 thinking 时别把 enable_thinking 发出去"的怪癖。

generate_kwargs 的合并规则(容易踩): provider 级参数是底,模型级参数覆盖在上,嵌套 dict 深合并;max_tokens 若没显式给就注入模型自己的(provider.py:336 get_effective_generate_kwargs)。返回的永远是新 dict,调用方改不脏 provider 状态。

"当前活跃模型"从哪来: ProviderManager.get_active_chat_model(providers/provider_manager.py:2392)= 取活跃模型槽(get_active_model,:1383)→ 找到对应 provider → 调它的 get_chat_model_instance。这就是工厂拿到"最内层模型"的入口。

3.4 模型路由:按 local/cloud 选槽(目前是脚手架)

要解决的小问题: 想让 agent 在"本地小模型"和"云端大模型"之间自动选。

实现: RoutingChatModel(agents/routing_chat_model.py:64)本身也是个 ChatModelBase,内部持有 local_endpointcloud_endpoint 两个 RoutingEndpoint(:55),__call__ 时先让 RoutingPolicy.decide(:34)选一条路,再把请求委托给对应 endpoint 的模型。

当前策略非常朴素: decidetext/channel/tools_available 参数直接 del 丢掉(routing_chat_model.py:41),只看配置里的 modelocal_first 还是 cloud_first 返回固定路由——还没有"按内容复杂度/成本"的真实路由逻辑。

诚实说明: 在本 commit 里,RoutingChatModel / RoutingEndpoint 没有被任何地方实例化——全仓搜索只有它自己的定义文件和 AgentsLLMRoutingConfig 配置项引用它(migration.py / routers/config.py / config.py)。也就是说,路由是已搭好骨架、配置项也在、但运行时装配路径尚未接线的能力。AgentBuilder.build_model(runtime/builder.py:323)走的是单模型的 create_model_and_formatter,不经过 router。(inferred:属"预留能力",非已启用主路径。)

3.5 多模态容错:两道防线

要解决的小问题: 用户可能塞图片/音视频,但当前模型不一定吃。硬发过去就是一个 400。

QwenPaw 在 QwenPawAgent._reasoning(agents/react_agent.py:401)里设了两道防线:

一次 _reasoning

┌───────────────────┴───────────────────┐
▼ 主动防线 ▼ 被动防线
调模型前先查: 真发出去 400 了?
模型支持多模态吗? →失败→ 是媒体相关错误吗?
或缓存里学过"拒收媒体"? │(react_agent.py:549
│是 │ _is_bad_request_or_media_error)
▼ ▼是
剥掉媒体块再发 往能力缓存写 rejects_media=True,
(或让 formatter 剥) 剥掉媒体,重试一次

关键细节:400 不能一律当"拒收媒体"。 _is_bad_request_or_media_error(react_agent.py:549)特意收紧:内容安全拒绝、请求过大、超上下文长度这些 400 都否决掉——因为把它们误判成"模型不支持媒体"会污染能力缓存,导致后续请求默默丢掉用户的图(注释 :558-566)。只有错误信息里真出现 image/audio/video/vision/multimodal 才算数。

学到的怪癖存哪: ModelCapabilityCache(providers/model_capability_cache.py:24)——进程级、不落盘、线程安全,key 是 provider:model。它只在"确认失败→恢复"之后才写(learn,:53),很保守。已知能力键:rejects_medianeeds_reasoning_content。另有一份 capability_baseline.py(:12 ExpectedCapability/ProbeSource)记录"官方文档声称的多模态能力"以及探测结果与文档不符时的 DiscrepancyLog

3.6 文本-only 自动续跑

要解决的小问题: 模型有时只回一句"好的,我来规划一下"就停了,既没调工具也没真干活。用户得再敲一句"继续"。

思路: _reasoning 拿到最终消息后,若判定该续跑(_should_auto_continue,react_agent.py:507:配置开了、这轮没有 tool_call、tool_choice != none、还没到 max_iters),就往上下文塞一条 user 角色的 system-hint(react_agent.py:476-503),然后直接 return——让 agentscope 的外层 ReAct 循环自然再转一轮。hint 内容按语言分中英两版(_AUTO_CONTINUE_HINT_ZH/EN,:328/339),还会把上一轮助手回复的尾部若干字符附进去当上下文(_auto_continue_tail_context,:355)。

妙在: 它不自己写 while 循环重试,而是"喂一条消息 + return",借力框架已有的外层循环(注释 react_agent.py:366-367)。

3.7 Token:计量 vs 估算,两件不同的事

初学者最容易混。QwenPaw 里"token"有两条完全独立的线:

计量(billing/统计)估算(触发压缩)
目的记"这次真花了多少 token"判"上下文快满了没"
数据来源模型响应里的真实 usage 字段字符数 / 除数 的近似
谁干的TokenRecordingModelWrapperEstimatedTokenCounter / model.count_tokens
落哪磁盘 JSON(按日/provider/model 聚合)只用于当下判断,不落盘

计量线: TokenRecordingModelWrapper._record_usage(token_usage/model_wrapper.py:35)从 ChatResponse.usage 抄出 input_tokens/output_tokens,enqueue 一个 _UsageEventTokenUsageManager(token_usage/manager.py:96)。这是同步 fire-and-forget(注释说 ~100ns,不 await),再由 TokenUsageBuffer(token_usage/buffer.py:29)异步批量刷盘。查询走 get_summary/get_details(manager.py:177/265),按日期区间聚合出 by_model/by_date。另外它还把最近一次用量按 session 存进内存(_store_usage,model_wrapper.py:70),供每轮回显。

顺带一个 vLLM 兼容小坑:model_wrapper.py:104tool_choice="auto" 改成 None——因为没开 --enable-auto-tool-choice 的 vLLM 会因为 auto 直接拒请求,置空后既绕过检查又保留 tools。

估算线: 运行时判断"要不要压缩上下文"时,agentscope 侧调 agent.model.count_tokens(**kwargs)(如 agents/middlewares.py:247、scroll 的 manager.py:156)。QwenPaw 自己的 EstimatedTokenCounter(agents/utils/estimate_token_counter.py:8)则是纯字符估算:len(text.encode("utf-8")) / divisor,除数可配(中文用 2-3、英文 4-5,:16-26)。每轮结束还会 snapshot_context_usage_for_state(token_usage/turn_usage.py:24)估一份上下文占用写进消息 metadata(TURN_USAGE_META_KEY,:14),给 UI 显示"上下文用了百分之多少"。

关于 tokenizer/ 目录: 仓库带了一份 Qwen 本地 tokenizer 词表(src/qwenpaw/tokenizer/tokenizer.json/vocab.json/merges.txt/tokenizer_config.json)。但在本 commit 的 src/ Python 代码里没找到直接加载这份词表做计数的路径——运行时的上下文估算走的是上面的字符估算 + model.count_tokens。这份词表更像是打包进去的数据资产(供 agentscope 的 Qwen/DashScope 计数或离线用途)。(inferred:未见直接 loader,故不能断言其运行时用途。)

3.8 上下文管理:压缩、卸载、pruning

长会话必然撑爆窗口。QwenPaw 有三个协作的机制:

① 压缩接管(compress_context,agents/react_agent.py:136)。 若注入了 context_manager(scroll 策略),压缩完全交给它;否则回落到 agentscope 原生压缩,且受 context_compact_config.enabled 开关控制。_save_to_context(:157)在每次写上下文后再通知 context_manager 把这些块"写穿"到它的存储。

② 卸载到磁盘(QwenPawOffloader,agents/offloader.py:29)。 它实现 agentscope 的 Offloader 协议,原生 compress_context 触发时自动把被驱逐的消息按日期追加到 {dialog_path}/{YYYY-MM-DD}.jsonl(offload_context,:46),把被截断的工具结果单独写成 {uuid}.txt(offload_tool_result,:105)。close() 时按保留天数清过期文件(cleanup_expired,:138)。

③ 工具结果分级截断(ToolResultPruningMiddleware,agents/middlewares.py:403)。 中间件形态,在 on_acting(:443,工具执行后)扫描上下文里所有 tool_result 块:最近 N 条用大阈值、更老的用小阈值,某些工具名/文件扩展名豁免走大阈值(:485 起)。截断前先把完整输出存成文件,保证可恢复(_truncate_tool_result,:597)。

3.9 coding / mission 两种 mode 怎么影响 agent

mode 是"往 prompt 里塞一段人格 + 往工具包里塞几个工具",不是换一个 agent 类。

Coding Mode:

  • 人格:_CODING_SYSTEM_PROMPT_TEMPLATE(modes/coding/mixin.py:27)是一大段行为规范——怎么建 *_TODO.md 追踪任务、代码引用用 path:line 形式、优先用 lsp/ast_search、文件操作必须用绝对路径、shell 命令要显式传 cwd。它由 CodingModeContributor(runtime/prompt_contributors.py:197,priority 85)在 coding mode 开启时拼进 prompt。
  • 工具:collect_coding_tools(modes/coding/mixin.py:271)按项目目录探测可用的 LSP 语言、检查 ast-grep CLI 在不在,分别加 lsp / ast_search 工具,都用 PolicyGuardedTool 包过守卫。
  • QwenPawAgent 继承了 CodingModeMixin(react_agent.py:40 的 MRO),但实际装配走的是 AgentBuilder._collect_coding_mode_tools(runtime/builder.py:491)那条独立函数路径——mixin 上同名方法是历史留存。

Mission Mode: 概念上同构——MissionPromptContributor(modes/mission/contributor.py:20,priority 25)在 mission 激活且有 mission state 时,调 build_mission_system_prompt 拼进一段任务指引。它委托给 agents/mission/prompts.py

两种 mode 的共同点:都不碰 agent 的核心循环,只在"prompt 片段"和"工具集合"两个可插拔点上加料


4. 生命周期:reply 与 close

一次回复(_reply)

QwenPawAgent._reply(agents/react_agent.py:627)只在原生 reply 前多做一件事:_inject_pending_hints(:615)——从 ToolCoordinator 弹出后台工具攒下的提示消息,append 进上下文,再走 super()._reply()。真正的推理循环在 agentscope 的 Agent 里,QwenPaw 通过重写 _reasoning(3.5/3.6 讲的两道防线 + 自动续跑)介入其中的每一步推理。

构造时还做了两件设置:把 agentscope 自带权限引擎设为 BYPASS(react_agent.py:127-129,因为 QwenPaw 用自己的 PolicyGuardedTool.check_permissions),以及注册每工具的默认超时到 ToolCoordinator(_register_tool_call_hooks,:633,如 shell 60s、子 agent 300s、browser 内部 3600s)。

关闭(close)

close(react_agent.py:234)按序释放:停 governor → 应用历史保留窗口并释放 scroll 的 sqlite 连接(否则长期运行的服务器会累积 fd)→ 清过期的工具结果文件。

会话持久化

state_dict/load_state_dict(react_agent.py:165/178)把 agent 状态序列化/恢复。load_state_dict 还兼容 1.x 旧格式:检测到 {"memory": {...}} 结构就现场迁移成 2.0 的 AgentState(:213-227),让老会话能平滑升级。


5. 巧妙之处(可借鉴的技术)

  1. 依赖全外注,agent 零自造。 测试时 mock 任一零件都无摩擦,装配逻辑集中在一个 AgentBuilder 里,agent 类保持纯粹(runtime/builder.py:91 vs agents/react_agent.py:52)。

  2. formatter 从实例类型派生,不查表。 _create_formatter_instance(model_factory.py:1163)读 model.formatter 的类型再动态建子类,连运行时拼的 compat 子类都自动适配,免维护脆弱映射。

  3. 关掉内层重试再套自己的。 显式 model.max_retries = 0(model_factory.py:1149),避免两层重试相乘造成 4×4 次无谓请求。

  4. 400 分诊,拒绝污染能力缓存。 _is_bad_request_or_media_error(react_agent.py:549)把"过大/超长/内容安全"从"媒体不支持"里摘出去,防止误学导致后续默默丢图。

  5. 自动续跑借力外层循环。 不写手动 while,只塞一条 hint 消息就 return,让框架的 ReAct 循环自然再转(react_agent.py:475-503)。

  6. prompt = 9 个可插拔 contributor。 加一段 prompt = 加一个 contributor + 定 priority,mode/scroll/driver 都靠这个点接入(runtime/prompt_contributors.py:297)。


6. 边界与局限

  • LLM 路由未接线。 RoutingChatModel 定义完整、配置项也在,但本 commit 无实例化路径,AgentBuilder.build_model 走单模型工厂;且现有 RoutingPolicy.decide 只看 mode 配置、丢弃全部内容信号(routing_chat_model.py:41)。
  • 能力缓存不落盘。 ModelCapabilityCache 是进程级的(model_capability_cache.py:41),进程重启后"学过的怪癖"清零,同一模型首次调用可能再失败一次。
  • token 计量依赖 provider 回传 usage。 若某 provider 的 ChatResponse.usage 缺失或为 0,_record_usage 直接跳过(model_wrapper.py:41),这次调用不计入统计。
  • 本地 Qwen tokenizer 词表运行时用途不明。 src/qwenpaw/tokenizer/ 有词表文件,但 src/ 里没见到直接加载它做计数的代码;上下文估算走字符近似(estimate_token_counter.py:40)。
  • coding mode 的 mixin 与装配双路径并存。 CodingModeMixin 在 agent 上,但真正注册工具走 AgentBuilder 的独立函数,mixin 同名方法属历史留存,读代码时容易误以为 mixin 在起作用。

7. 横向对比

ai-agent-reference 货架里,"agent 本体如何在一个第三方 ReAct 框架之上叠加生产行为"是共性关切。QwenPaw 的取舍有两个鲜明特征:

  • 薄 agent + 厚装配工。 不少同类项目把模型选择、prompt 拼装、工具注册都写进 agent 类;QwenPaw 反其道,agent 只叠加行为,全部构造外移到 AgentBuilder
  • 包装而非改造 provider。 重试/限流/计量三层都是 ChatModelBase 装饰器(model_factory.py:1153-1158),不动 agentscope 原生模型,升级上游时冲突面小。

具体对比表见货架总库 doc 的"agent 组装与模型接入"一节。


8. 代码地图(导航索引)

用符号名 grep 比行号抗漂移。

主题文件路径符号名
Agent 本体src/qwenpaw/agents/react_agent.pyQwenPawAgent
推理拦截(媒体/续跑)src/qwenpaw/agents/react_agent.py_reasoning, _should_auto_continue
400 分诊src/qwenpaw/agents/react_agent.py_is_bad_request_or_media_error
生命周期src/qwenpaw/agents/react_agent.py_reply, close, state_dict, load_state_dict
装配工src/qwenpaw/runtime/builder.pyAgentBuilder.build, build_model, build_prompt
模型工厂 + 三层包装src/qwenpaw/agents/model_factory.pycreate_model_and_formatter, _create_formatter_instance
formatter 兼容层src/qwenpaw/agents/model_factory.py_create_file_block_support_formatter
Provider 抽象src/qwenpaw/providers/provider.pyProvider, ModelInfo, get_chat_model_instance
Provider 管理器src/qwenpaw/providers/provider_manager.pyProviderManager.get_active_chat_model, get_active_model
DashScope provider 实例化src/qwenpaw/providers/dashscope_provider.pyDashScopeProvider.get_chat_model_instance, _DashScopeChatModelCompat
模型路由(脚手架)src/qwenpaw/agents/routing_chat_model.pyRoutingChatModel, RoutingPolicy
重试 + 限流src/qwenpaw/providers/retry_chat_model.pyRetryChatModel, RetryConfig, RateLimitConfig
能力缓存src/qwenpaw/providers/model_capability_cache.pyModelCapabilityCache
能力基线/探测src/qwenpaw/providers/capability_baseline.pyExpectedCapability, ProbeSource
prompt 组装(现主路径)src/qwenpaw/runtime/prompt_contributors.py_ALL_CONTRIBUTORS, build_default_prompt_manager
prompt 排序器src/qwenpaw/runtime/prompt_manager.pyPromptManager.build_sync
工作目录 prompt(旁路)src/qwenpaw/agents/prompt.pybuild_system_prompt_from_working_dir, PromptBuilder
host 锚点 prompt(旁路)src/qwenpaw/agents/prompt_builder.pyPromptBuilder, HOST_ANCHORS
模板种子src/qwenpaw/agents/templates.pybuild_agent_template
token 计量src/qwenpaw/token_usage/model_wrapper.pyTokenRecordingModelWrapper._record_usage
token 聚合/查询src/qwenpaw/token_usage/manager.pyTokenUsageManager, get_summary
token 缓冲落盘src/qwenpaw/token_usage/buffer.pyTokenUsageBuffer, _UsageEvent
每轮上下文估算src/qwenpaw/token_usage/turn_usage.pysnapshot_context_usage_for_state
字符估算器src/qwenpaw/agents/utils/estimate_token_counter.pyEstimatedTokenCounter
上下文卸载src/qwenpaw/agents/offloader.pyQwenPawOffloader
工具结果分级截断src/qwenpaw/agents/middlewares.pyToolResultPruningMiddleware, MemoryMiddleware
Coding Modesrc/qwenpaw/modes/coding/mixin.pyCodingModeMixin, _CODING_SYSTEM_PROMPT_TEMPLATE, collect_coding_tools
Mission Modesrc/qwenpaw/modes/mission/contributor.pyMissionPromptContributor