Profiles 功能的核心解读
核心痛点:频繁换模型 = 代码地狱
开发者在使用不同大模型(如 OpenAI、Anthropic Claude 等)时,会遇到一个普遍问题:
每次更换模型,都要修改 create_deep_agent 核心调用代码,调整参数、提示词格式等,过程麻烦且容易出错,导致代码冗余、维护成本高。
Profiles:模型的 “万能适配卡”
Profiles 就是为了解决上面的问题而设计的。它的核心思想是:
- 提前为某个模型供应商或具体模型写好一整套 “微调规则”
- Deep Agents 运行时会自动识别并匹配对应的模型,自动应用这些规则
- 优势:完全不影响智能体的核心业务逻辑,让代码更清爽、配置更集中。
两种 Profile 的分工(核心差异)
框架提供了两种 Profile,它们的作用完全不同,面向的开发者场景也不一样:
| 类型 | 定位 | 作用范围 | 适用场景 |
|---|---|---|---|
| Harness Profile | 调整运行时行为 | 控制智能体的业务逻辑层面 | 绝大多数开发者的 “主战场” |
| Provider Profile | 控制模型构建过程 | 控制模型的底层初始化与参数 | 封装第三方供应商集成时使用 |
🔧 Harness Profile:业务逻辑的 “调节器”
它主要用来控制智能体在运行时的行为,包括:
- 系统提示词的拼接方式
- 工具的可见性(哪些工具对模型开放)
- 中间件的增删
- 通用子智能体的表现规则
一句话总结:它是你用来 “调教” 智能体行为的地方,是日常开发中最常用的配置。
⚙️ Provider Profile:模型的 “幕后帮手”
它更偏向底层,用来管理 init_chat_model 过程中的细节:
- 设置模型的默认参数(如温度、最大 token 数)
- 初始化前的检查逻辑(比如验证 API 密钥)
- 动态参数的获取(比如从配置中心读取模型地址)
一句话总结:它主要用于封装和适配不同的模型供应商,普通开发者很少直接用到。
对绝大多数使用者来说,Harness Profile 就是主战场,能满足你对智能体行为的各种微调需求。Provider Profile 通常只在封装第三方供应商集成时才会用到。
这个 Profiles 机制本质上是一种关注点分离的设计:
业务逻辑(智能体行为) 由 Harness Profile 管理,和模型无关
模型底层适配 由 Provider Profile 管理,和业务逻辑无关
这样一来,你切换模型时,只需要新增 / 修改对应的 Provider Profile,业务代码完全不用动,实现了真正的解耦。
Harness Profile :智能体行为的“遥控器”
一个简单的例子:
你想给 openai:gpt-5.4 这个模型做几个定制化设置:
- 回答要尽量简短(不超过 100 字)
- 隐藏 / 禁用
execute这个工具 - 移除
SummarizationMiddleware这个摘要中间件 - 关闭通用子智能体功能
1 | from deepagents import ( |
导入模块
GeneralPurposeSubagentProfile:控制通用子智能体的配置HarnessProfile:核心的 “行为配置类”,用来定义智能体的运行时规则register_harness_profile:注册配置的函数,让框架知道哪个模型用这套规则
register_harness_profile函数第一个参数
"openai:gpt-5.4":指定这套规则只对这个模型生效第二个参数是
HarnessProfile对象,里面包含了所有定制规则:
| 参数 | 作用 | 效果 |
|---|---|---|
system_prompt_suffix |
给系统提示词追加内容 | 强制模型回答不超过 100 字 |
excluded_tools={"execute"} |
把指定工具从模型可用列表中移除 | 模型再也看不到、也无法调用 execute工具 |
excluded_middleware={"SummarizationMiddleware"} |
禁用指定的中间件 | 去掉自动摘要功能 |
general_purpose_subagent=...(enabled=False) |
配置通用子智能体 | 完全关闭子智能体功能 |
这段代码一注册,Deep Agents 就会自动执行所有配置,而你的核心业务代码
create_deep_agent(...)可以完全不改动。
这就是 Profiles 设计的核心优势:
- 关注点分离:业务逻辑和模型行为配置完全解耦
- 可复用性:同一套规则可以在多个地方复用,不用重复写配置
- 易维护:要改模型行为,只需要修改 Profile 配置,不用动业务代码,大幅降低出错风险
HarnessProfile 可配置的字段和关键规则
| 字段 | 作用通俗解释 | 典型用法示例 |
|---|---|---|
base_system_prompt |
直接替换框架内置的基础系统提示词,相当于重写智能体的 “初始人格” | 把默认的通用助手人格,改成 “资深 Python 代码审查员” |
system_prompt_suffix |
在所有提示词的最后面追加文本,主智能体和子智能体都会生效 | 强制模型全程用中文回答、只返回 JSON 格式 |
tool_description_overrides |
用字典形式,覆盖单个工具的描述文本 | 把默认英文的工具说明改成中文,方便模型理解 |
excluded_tools |
从模型的可用工具列表中,移除指定的工具(发生在工具列表组装的最后一步) | 禁用敏感工具(如文件删除、系统执行),防止误操作 |
excluded_middleware |
从默认中间件栈中删除指定中间件(支持类名、字符串或导入引用) | 去掉自动摘要、日志记录等非必要中间件 |
extra_middleware |
给中间件栈追加自定义中间件(支持静态列表或动态生成的可调用对象) | 接入自定义的权限校验、日志上报中间件 |
general_purpose_subagent |
接收一个 GeneralPurposeSubagentProfile 对象,控制内置通用子智能体的开关、名字和系统提示词 |
关闭子智能体功能、修改子智能体的默认行为 |
1. 提示词的执行顺序是 “铁律”
调用方传入的
system_prompt永远排在最前面,而system_prompt_suffix永远在最后面。不管模型怎么切换、Profile 怎么覆盖,这个顺序都不会变。
提示词的完整拼接顺序:
flowchart LR A["调用方传入的 system_prompt
(最外层,最先拼接)"] --> B["base_system_prompt
(替换/覆盖框架内置人格)"] B --> C["其他框架内置内容
(如工具说明、中间件注入的上下文)"] C --> D["system_prompt_suffix
(全局指令,最后拼接,强制生效)"]
- 影响:如果你想强制模型遵守某个规则(比如 “必须用中文回答”),放在
system_prompt_suffix里是最稳妥的,因为它永远是最后生效的,不会被前面的内容覆盖。
2. 彻底禁用子智能体的正确方式
只设置 general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False) 还不够!
你还必须在调用 create_deep_agent 时,确保 subagents= 参数为空,否则 task 这类工具还是会出现。
- 错误做法:只关 Profile 里的子智能体开关
- 正确做法:Profile 里禁用 +
create_deep_agent不传任何子智能体配置
3. 骨架级中间件不能被排除
FilesystemMiddleware、SubAgentMiddleware 以及内部的权限中间件,是框架的核心骨架,不能放进 excluded_middleware,否则会直接抛出 ValueError。
- 正确做法:如果不想让模型使用这些中间件的功能,应该用
excluded_tools隐藏对应的工具,而不是直接删除中间件本身。
excluded_middleware排除中间件的三种写法
1. 直接传类
- 写法示例:
SummarizationMiddleware - 原理:系统通过精确的类型匹配来排除中间件。
- 优点:最直接、类型安全,IDE 可以自动补全,写错会直接报错。
- 缺点:只能在代码中使用,不适合写在配置文件里。
- 适用场景:Python 代码中直接配置,开发调试阶段首选。
1 | # 示例 |
2. 传字符串名称
- 写法示例:
"SummarizationMiddleware" - 原理:匹配中间件类的
name属性,只要name对上就会被排除。 - 优点:写法简单,不用导入模块,适合快速配置。
- 缺点:如果有重名的中间件,可能会误删;IDE 无法自动校验。
- 适用场景:快速排除已知名称的中间件,或者简单配置场景。
1 | # 示例 |
3. 传导入引用(模块:类名)
- 写法示例:
"my_pkg.middleware:TelemetryMiddleware" - 原理:用
模块路径:类名的格式,动态导入并匹配中间件。 - 优点:可以精确指定任意模块的中间件,支持在 YAML/JSON 等配置文件中写配置,不用提前导入。
- 缺点:写法稍长,IDE 无法直接校验,写错会在运行时才报错。
- 适用场景:配置文件(如 YAML)中动态配置,或者需要排除自定义模块中的中间件。
1 | # 示例 YAML 配置 |
三种方式对比表
| 方式 | 格式 | 优点 | 缺点 | 最佳场景 |
|---|---|---|---|---|
| 直接传类 | SummarizationMiddleware |
类型安全、IDE 友好 | 只能在代码中使用 | Python 代码内配置、开发阶段 |
| 传字符串名称 | "中间件类名" |
写法简单、无需导入 | 可能重名误删、无类型校验 | 快速排除已知中间件 |
| 传导入引用 | "模块:类名" |
精确匹配、支持配置文件 | 运行时才校验、IDE 不友好 | YAML/JSON 配置、自定义中间件 |
💡 关键补充说明
- 优先级:这三种方式的排除效果是一样的,框架会同时支持三种匹配逻辑,选你觉得方便的就行。
- 避坑提醒:像
FilesystemMiddleware这类骨架级中间件是不能被排除的,强行排除会直接抛ValueError,正确的做法是用excluded_tools隐藏它对应的工具。
Deep Agents 中 Profile 的注册键层级与配置合并逻辑
核心是 “通用配置打底,专用配置叠加覆盖”。
注册键的两种层级
Profile 的注册键分为两个层级,作用范围不同:
| 层级 | 写法示例 | 生效范围 |
|---|---|---|
| 供应商级别 | openai |
配置对该供应商下所有模型生效,相当于 “全局默认配置” |
| 模型级别 | openai:gpt-5.4 |
配置仅对特定模型生效,相当于 “模型专属定制配置” |
核心合并逻辑(优先级规则)
当你同时注册了供应商级和模型级的 Profile 时,框架会按以下顺序自动合并配置:
1️⃣ 供应商级打底
先用供应商级别的配置作为基础模板,比如给 openai 配置了 “默认用中文回答”“禁用 execute 工具”,所有 OpenAI 模型都会先继承这一套配置。
2️⃣ 模型级覆盖
模型级配置会和供应商级配置合并,规则是:
- 模型级未设置的字段:继承供应商级配置
- 模型级已设置的字段:直接覆盖供应商级配置
- 例:供应商级配置了
system_prompt_suffix="用中文回答",模型级配置了system_prompt_suffix="用中文回答,且只返回JSON",最终会使用模型级的版本。
3️⃣ 重复注册继续合并
即使对同一个键(比如 openai:gpt-5.4)多次注册 Profile,框架也不会直接替换旧配置,而是继续叠加合并。
- 例:第一次注册配置了 “禁用 execute 工具”,第二次注册配置了 “禁用 search 工具”,最终两个工具都会被禁用。
示例代码
1 | from deepagents import HarnessProfile, register_harness_profile |
最终合并后的配置效果
| 字段 | 最终值 | 来源说明 |
|---|---|---|
system_prompt_suffix |
"用中文回答,且只返回JSON格式。" |
模型级覆盖了供应商级 |
excluded_tools |
{"execute", "search"} |
供应商级的 execute + 第二次注册的 search,叠加生效 |
excluded_middleware |
{"SummarizationMiddleware"} |
模型级新增,供应商级未设置,所以直接保留 |
这种分层 + 合并的设计,让你可以:
- 用供应商级配置统一管控同品牌模型的通用规则(比如语言、工具限制)
- 用模型级配置给特定模型做个性化微调,不影响其他模型
- 用多次注册实现模块化配置(比如按功能拆分不同的 Profile,最后合并生效)
各字段合并方式
| 字段 | 所属 Profile 类型 | 合并方式 | 通俗理解 |
|---|---|---|---|
base_system_prompt |
Harness | 新值一旦设置就直接覆盖旧值;未设置则保持原样 | 非黑即白,要么完全继承,要么完全替换 |
system_prompt_suffix |
Harness | 新值一旦设置就直接覆盖旧值;未设置则保持原样 | 优先级最高的指令,写了就会直接替换 |
tool_description_overrides |
Harness | 按工具名合并字典,同一工具名的新描述覆盖旧描述 | 增量更新工具说明,只改指定工具 |
excluded_tools |
Harness | 取并集,新旧排除项叠加 | 越配置禁用的工具越多,不会被覆盖 |
excluded_middleware |
Harness | 取并集,新旧排除项叠加 | 越配置禁用的中间件越多,不会被覆盖 |
extra_middleware |
Harness | 按中间件类合并:同类型在原位置替换,新类型追加到末尾 | 重复的替换,新的往后排 |
general_purpose_subagent |
Harness | 逐字段合并,未设置的字段沿用原值 | 只更新你指定的子智能体配置,其他不变 |
init_kwargs |
Provider | 按字典键合并,同一键的新值覆盖旧值 | 增量更新模型初始化参数 |
pre_init |
Provider | 按顺序串联执行:原有先执行,新的接在后面 | 初始化钩子按注册顺序依次执行 |
init_kwargs_factory |
Provider | 工厂函数按顺序串联,解析模型时合并所有工厂的输出 | 多个参数工厂的输出会合并为最终配置 |
1. base_system_prompt / system_prompt_suffix
合并规则:新值一旦设置就直接覆盖旧值;没设置的话保持原样。
通俗理解:非黑即白,要么完全继承旧值,要么完全替换。
示例:
1
2
3
4
5# 供应商级
HarnessProfile(system_prompt_suffix="用中文回答。")
# 模型级
HarnessProfile(system_prompt_suffix="用中文回答,只返回JSON。")
# 最终结果:"用中文回答,只返回JSON。"(模型级直接覆盖)
2. tool_description_overrides
合并规则:按工具名合并字典,同一个工具名上新描述覆盖旧描述。
通俗理解:工具描述的 “增量更新”,只改你指定的工具,其他不变。
示例:
1
2
3# 旧配置:{"search": "搜索工具", "execute": "执行工具"}
# 新配置:{"search": "全网搜索工具"}
# 最终结果:{"search": "全网搜索工具", "execute": "执行工具"}
3. excluded_tools / excluded_middleware
合并规则:取并集,新旧排除项叠加,不会漏掉。
通俗理解:越配置,禁用的东西越多,不会被覆盖。
示例:
1
2
3# 旧配置:{"execute"}
# 新配置:{"search"}
# 最终结果:{"execute", "search"}(两个都被排除)
4. extra_middleware
合并规则:按中间件具体类合并:
- 同类型则在原位置替换
- 全新类则追加到末尾
通俗理解:重复的替换,新的往后排。
示例:
1
2
3
4# 旧配置:[LoggingMiddleware, AuthMiddleware]
# 新配置:[LoggingMiddleware, TelemetryMiddleware]
# 最终结果:[LoggingMiddleware, AuthMiddleware, TelemetryMiddleware]
# (LoggingMiddleware被替换,TelemetryMiddleware追加到末尾)
5. general_purpose_subagent
合并规则:逐字段合并,未设置的字段沿用原来的值。
通俗理解:子智能体配置的 “部分更新”,只改你指定的字段。
示例:
1
2
3# 旧配置:enabled=True, name="助手", system_prompt="你是助手"
# 新配置:system_prompt="你是代码助手"
# 最终结果:enabled=True, name="助手", system_prompt="你是代码助手"
6. init_kwargs(Provider 专用)
合并规则:按字典键合并,同一个键上新值覆盖旧值。
通俗理解:模型初始化参数的 “增量更新”,类似
tool_description_overrides。示例:
1
2
3# 旧配置:{"temperature": 0.7, "max_tokens": 1000}
# 新配置:{"temperature": 0.2}
# 最终结果:{"temperature": 0.2, "max_tokens": 1000}
7. pre_init(Provider 专用)
合并规则:按顺序串联执行:原有的先执行,新的接在后面。
通俗理解:初始化前的钩子函数会按注册顺序依次执行,不会被覆盖。
示例:
1
2
3# 旧pre_init:检查API密钥
# 新pre_init:记录初始化日志
# 执行顺序:检查API密钥 → 记录初始化日志
8. init_kwargs_factory(Provider 专用)
合并规则:工厂函数按顺序串联,每次解析模型时合并所有工厂的输出。
通俗理解:多个参数工厂的输出会合并成最终的
init_kwargs。示例:
1
2
3# 工厂1输出:{"temperature": 0.7}
# 工厂2输出:{"max_tokens": 1000}
# 最终init_kwargs:{"temperature": 0.7, "max_tokens": 1000}
💡 核心避坑指南
- 覆盖型 vs 叠加型:
- 覆盖型:
base_system_prompt/system_prompt_suffix、init_kwargs、tool_description_overrides→ 新值会替换旧值,只保留你最后设置的内容。 - 叠加型:
excluded_tools/excluded_middleware、extra_middleware、pre_init→ 配置会越来越多,不会被替换。
- 覆盖型:
- 子智能体配置是 “部分更新”:
general_purpose_subagent只会修改你指定的字段,其他字段会继承旧值,不会重置。 - Provider 钩子的顺序性:
pre_init和init_kwargs_factory都是按注册顺序执行的,顺序会影响最终效果。
预配置模型实例的 Profile 查找顺序
预配置模型实例的 Profile 查找顺序
当你把一个已经构造好的聊天模型实例传给 create_deep_agent 时,Deep Agents 会按下面的优先级顺序匹配 Profile:
精确匹配(最高优先级)
匹配方式:用完整的
provider:identifier键进行匹配,比如openai:gpt-5.4效果:只有完全匹配这个键的 Profile 才会被选中,优先级最高
Identifier 匹配(次优先级)
匹配方式:仅用
identifier部分匹配(前提是identifier本身包含:,比如模型标识是gpt-5:4)效果:当完整键匹配不到时,会尝试用模型的标识符部分单独匹配 Profile
Provider 降级匹配(最低优先级)
匹配方式:降级到仅用
provider(供应商)匹配,比如openai效果:如果前面两种都匹配不到,就会用供应商级别的 Profile 作为兜底配置
为什么没有 “一键全选” 的通配键?
Profiles 的设计初衷是与特定模型绑定,因此不存在类似 * 的通配符键。
- 如果你需要 “无论用什么模型都生效” 的全局调整,不能靠 Profiles
- 正确做法:直接写在
create_deep_agent的调用参数里(比如直接传system_prompt、excluded_tools等参数),而不是通过 Profiles 配置
💡 核心逻辑总结
- 匹配优先级:完整模型键 > 模型标识符 > 供应商键,框架会从最具体到最通用依次尝试匹配。
- Profiles 是模型绑定的:Profiles 只能针对特定供应商或模型生效,不能实现真正的 “全局所有模型生效”,全局配置要写在调用参数里。
Provider Profile:决定模型怎么“出生“
ProviderProfile 是什么?
ProviderProfile 是 Deep Agents 中专门负责模型初始化过程的配置,和 HarnessProfile 不同:
HarnessProfile管的是模型运行时的行为(提示词、工具、中间件等)ProviderProfile只影响模型被创建的那一瞬间,也就是init_chat_model的过程
⚠️ 关键前提:它只有在你用字符串 "供应商:模型" 的方式让框架自己初始化模型时才会生效;如果你直接传入一个已经构造好的模型实例,它是不起作用的。
示例代码
1 | from deepagents import ProviderProfile, register_provider_profile |
这段代码的效果:所有 OpenAI 模型在被框架初始化时,都会默认把
temperature设置为0(即模型输出更确定、少随机)。后续你调用
create_deep_agent(model="openai:gpt-4o")时,框架会自动把这个参数带上。
核心字段详解
| 字段 | 作用 | 通俗理解 | 典型场景 |
|---|---|---|---|
init_kwargs |
静态参数字典,原封不动传给 init_chat_model |
给模型设置固定的默认参数 | 设置 temperature、max_tokens、top_p等固定超参数 |
pre_init |
可调用对象,在模型初始化之前执行 | 模型创建前的 “钩子”,用来做检查和准备 | 检查 API 密钥是否存在、验证环境变量、打印初始化日志 |
init_kwargs_factory |
无参数可调用对象,返回参数字典,每次解析模型时重新调用 | 动态生成初始化参数 | 从配置中心动态获取模型地址、从上下文读取用户自定义参数 |
合并逻辑规则
三个字段的合并方式:
init_kwargs:按字典键合并,同一个键上新值覆盖旧值(类似普通字典更新)。- 例:供应商级设置
{"temperature": 0.7},模型级设置{"temperature": 0.2},最终temperature为0.2。
- 例:供应商级设置
pre_init:按顺序链式执行,所有注册的pre_init函数都会依次执行,不会被覆盖。- 例:第一个
pre_init检查密钥,第二个pre_init打印日志,两个都会按顺序运行。
- 例:第一个
init_kwargs_factory:按顺序链式执行,每次解析模型时,所有工厂函数的输出都会合并成最终的init_kwargs。- 例:工厂 1 返回
{"temperature": 0.7},工厂 2 返回{"max_tokens": 1000},最终合并为{"temperature": 0.7, "max_tokens": 1000}。
- 例:工厂 1 返回
ProviderProfile 的核心价值,是把模型初始化的细节从业务代码中抽离出来:
- 你可以集中管理不同供应商 / 模型的默认参数
- 可以统一做初始化前的校验和准备工作
- 支持动态参数注入,而不用修改核心业务代码
完整的示例代码
1 | from deepagents import ( |
用配置文件管理:告别硬编码
背景:为什么需要 YAML/JSON 配置?
在开发阶段,直接把 HarnessProfile 写在 Python 代码里很方便,但到了生产环境会有几个问题:
- 配置和业务代码耦合,改配置要重新发布、部署
- 多环境(开发 / 测试 / 生产)的配置差异不好管理
- 非开发人员(比如运维、产品)无法直接修改配置
HarnessProfileConfig 就是为了解决这个问题,它把 HarnessProfile 里的纯数据部分提取出来,支持用 YAML/JSON 文件配置,而运行时的 Python 对象逻辑仍然留在 HarnessProfile 中。
YAML配置示例
openai.yaml
1 | # 替换基础系统提示词,相当于给智能体重写初始人格 |
核心优势与用法
- 数据与逻辑分离
- YAML/JSON 文件:只存纯数据配置(字符串、列表、字典等),任何人都能看懂和修改
- Python 代码:保留运行时逻辑(比如中间件类、函数钩子),不暴露给配置文件
- 如何在代码中加载 YAML 配置?
下面是对应的 Python 加载示例,你可以直接用:
1 | import yaml |
导入依赖
import yaml:用于解析 YAML 配置文件HarnessProfileConfig:专门用来加载纯数据配置的类register_harness_profile:注册配置的核心函数
加载并注册配置
with open("openai.yaml") as f:打开 YAML 配置文件yaml.safe_load(f):安全解析 YAML 文件,得到 Python 字典HarnessProfileConfig.from_dict(...):把字典转换成框架可识别的配置对象register_harness_profile("openai", ...):把配置注册给openai供应商级别的所有模型
配置文件只能承载纯声明式的数据。如果 Profile 里包含了用代码写的中间件实例、工厂函数,或者是类形式的 excluded_middleware 条目,那还是得回到 Python 里用 HarnessProfile 注册。
关键说明
支持的字段:YAML/JSON 中只能配置
HarnessProfile里的纯数据字段,比如:base_system_prompt/system_prompt_suffixexcluded_tools/excluded_middlewaretool_description_overridesgeneral_purpose_subagent的基础配置而
extra_middleware这类需要 Python 对象的字段,还是要在代码中配置。
合并逻辑不变:通过
HarnessProfileConfig注册的配置,和直接在代码中写的HarnessProfile遵循完全一样的合并规则(供应商级→模型级叠加覆盖)。