1. 本文目标与环境基线
1.1 本文要解决的问题
一个能够长期使用的 Deep Agents 开发环境,至少要回答下面几个问题:
- 使用哪个 Python 和 deepagents 版本;
- 如何隔离、锁定和验证依赖;
- 模型需要具备哪些能力;
- 如何安全保存 Provider 配置;
- 如何区分标准 OpenAI API 与第三方兼容端点;
- 如何确认模型真的产生了 Tool Call;
- 哪些测试需要真实凭据,哪些只需要占位配置。
本文完成后,会得到一个具有锁文件、安全配置、离线测试和真实 Tool Calling 证据的示例工程。
1.2 固定基线与适用范围
| 项目 | 本系列基线 | 说明 |
|---|---|---|
| Python | 3.12 | 创建环境时显式指定次版本 |
| 环境工具 | uv | 创建虚拟环境、解析依赖和同步锁文件 |
| Deep Agents | deepagents==0.6.12 | 本系列的稳定版主线 |
| LangChain OpenAI 集成 | langchain-openai==1.3.5 | 来自当前锁文件 |
| OpenAI Python SDK | openai==2.45.0 | 来自当前锁文件 |
| 默认模型别名 | qwen3.7-plus | 由 DEFAULT_MODEL 统一管理 |
| 测试 | Pytest + Ruff + Compileall | 分别验证行为、代码质量和语法 |
表中的模型 ID 是本文测试网关返回的名称,可能是网关别名,不代表模型厂商的公开 API
一定存在同名模型。实际可用模型以端点的 /models 返回结果为准。
2. 创建可复现的开发环境
2.1 为什么使用独立虚拟环境
博客仓库还包含 LangChain、LangGraph 和其他 Python 示例。如果所有文章共用一个环境,
某次 Provider 升级很可能间接更新 LangChain 或 Pydantic,导致其他示例失效。
本系列使用:
source/_posts/deepagent/examples/.venv_deepagent
虚拟环境本身不提交到 Git。应该提交的是直接依赖意图、已解析的锁文件和不含真实密钥的配置模板。
2.2 使用 uv 创建 Python 3.12 环境
python3.12 --version
uv --version
cd source/_posts/deepagent/examples
uv venv --python 3.12 .venv_deepagent
不要依赖 python 当前碰巧指向哪个解释器。即使不执行 source …/activate,后续命令也可以
直接使用 .venv_deepagent/bin/python,确保解释器来源明确。

这条链路应该按层排错:Python 失败时不必检查 Provider,依赖冲突时不必调试 Tool,
离线测试没有通过时也不应该先花费模型 Token。
2.3 直接依赖与锁文件
requirements.in 记录项目主动选择的直接依赖:
deepagents==0.6.12
deepagents-acp
langchain-openai
langchain-mcp-adapters
langchain-daytona
mcp
python-dotenv
langgraph-cli[inmem]
pytest
ruff
skills-ref==0.1.1
使用下面的命令生成并同步锁文件:
uv pip compile requirements.in -o requirements-lock.txt
uv pip sync \
--python .venv_deepagent/bin/python \
requirements-lock.txt
uv pip check --python .venv_deepagent/bin/python
这里的“可复现”需要分成两件事:
- 使用已提交的 requirements-lock.txt 重建环境,可以复现当前已解析的版本集合;
- 未来重新执行 uv pip compile requirements.in 是一次升级操作,未锁范围的直接依赖可能得到新版本,不保证生成与旧锁文件完全一致的结果。
当前锁文件首先服务于本系列的 macOS 和 Python 3.12 测试环境。跨 Linux、Windows
或不同 CPU 架构时,还要验证 Environment Marker、Wheel 可用性和平台专属包;不应该把
某一平台的安装成功等同于跨平台保证。
当前环境实际检查 136 个包,uv pip check 返回全部兼容。
2.4 示例工程目录
examples/
├── .env.example
├── .venv_deepagent/ # 本地环境,不提交
├── requirements.in
├── requirements-lock.txt
├── conftest.py # 离线测试占位配置
├── shared/
│ ├── __init__.py
│ └── config.py
└── p02_quickstart/
├── __init__.py
├── quickstart.py
├── model_options.py
├── live_demo.py
└── test_quickstart.py
本项目显式保留 init.py,因此 python -m p02_quickstart.quickstart 的包边界清晰,不依赖
Namespace Package 的隐式行为。
3. 环境变量与密钥安全
3.1 .env.example 设计
cp .env.example .env
核心配置如下:
DEEPAGENT_API_KEY=
DEEPAGENT_BASE_URL=https://your-openai-compatible-endpoint.example/v1
DEEPAGENT_PROVIDER_PROFILE=openai-compatible
DEEPAGENT_MODEL=qwen3.7-plus
DEEPAGENT_FAST_MODEL=qwen3.6-flash
DEEPAGENT_VISION_MODEL=qwen3.7-plus
DEEPAGENT_GRADER_MODEL=qwen3.7-max
| 变量 | 用途 |
|---|---|
| DEEPAGENT_API_KEY | 端点认证,不得提交 |
| DEEPAGENT_BASE_URL | 声称兼容 OpenAI API 的端点地址 |
| DEEPAGENT_PROVIDER_PROFILE | 显式决定是否发送 Provider 私有字段 |
| DEEPAGENT_MODEL | 主 Agent 使用的模型 ID |
| DEEPAGENT_FAST_MODEL | 低延迟任务模型 ID |
| DEEPAGENT_VISION_MODEL | 图片等多模态任务模型 ID |
| DEEPAGENT_GRADER_MODEL | 质量评估模型 ID |
DEEPAGENT_* 是 Provider-neutral 的外层变量命名,不代表底层实现完全没有供应商适配。
本文是“通用基础配置 + 显式 Provider Profile”,而不是仅凭模型名自动猜测端点能力。
3.2 加载优先级
load_dotenv 默认不会覆盖已存在的进程环境变量,因此优先级是:
CI / 生产 Secret Manager 注入的进程变量
> examples/.env
> 代码中的非敏感默认值
.env 只用于本地开发,必须被 Git 忽略。代码、错误日志、Trace、Draw.io 和文章运行结果
都不应包含密钥、Authorization Header 或带认证信息的代理 URL。
3.3 离线测试中的占位配置
“离线测试不需要密钥”的准确含义是:不需要真实有效凭据,但构造 ChatOpenAI
对象仍然需要非空 API Key 和 Base URL。Pytest 通过 conftest.py 注入明确的占位值:
import os
os.environ.setdefault("DEEPAGENT_API_KEY", "test-key-not-sent")
os.environ.setdefault("DEEPAGENT_BASE_URL", "https://example.invalid/v1")
os.environ.setdefault("DEEPAGENT_MODEL", "qwen3.7-plus")
os.environ.setdefault("DEEPAGENT_FAST_MODEL", "qwen3.6-flash")
os.environ.setdefault("DEEPAGENT_GRADER_MODEL", "qwen3.7-max")
os.environ.setdefault("DEEPAGENT_PROVIDER_PROFILE", "openai-compatible")
.invalid 是专门保留的无效域名,这些测试只构造对象和图,不发送网络请求。
| 测试类型 | 真实凭据 | 占位配置 |
|---|---|---|
| 工具单元测试 | 不需要 | 不需要 |
| 模型对象构造 | 不需要 | 需要 |
| Agent 图装配 | 不需要 | 需要 |
| Provider 集成与 Tool Calling E2E | 需要 | 需要真实端点配置 |
4. 如何为 Deep Agents 选择模型

4.1 Tool Calling 是硬要求
Deep Agents 不限定某个 Provider,但主模型必须能根据工具名称、描述和 JSON Schema 生成
结构化 Tool Call。Models 文档也将
Tool Calling 列为基本要求。模型只输出“我准备调用工具”这段文本,并不能驱动 Agent Loop。
除基本 Tool Calling 外,还应该按项验证:
- 嵌套对象、枚举、可选字段和并行 Tool Call;
- 是否支持强制 tool_choice;
- Pydantic 或 JSON Schema 结构化输出;
- 流式 Token 与流式 Tool Call;
- 图片等多模态内容块;
- Usage Metadata、错误类型、429 和 5xx 行为;
- 单次延迟、总任务 Token、并发限额和成本。
4.2 模型角色的最小拆分
| 角色 | 主要任务 | 选择重点 |
|---|---|---|
| 主模型 | 规划、Tool Calling、结果综合 | 指令遵循、工具稳定性和上下文能力 |
| 快速模型 | 低风险、低延迟的辅助任务 | 延迟、成本与基本 Tool Calling |
| 视觉模型 | 图片、图表和多模态文件 | 内容块格式和识别质量 |
| Grader | 质量评估 | 结构化输出和评价稳定性 |
详细的 Rubric 与偏差控制放在第 11 篇讨论。本篇只建立“不同角色可以使用不同模型”的配置基础。
4.3 模型字符串与模型实例
字符串方式通常使用 provider:model 形式:
agent = create_deep_agent(model="openai:your-model-id")
这只是示意。实际使用时必须安装对应 Provider Integration 包,并设置该 Provider 所需的
认证变量。不同 Provider 的前缀、默认端点和初始化参数不同;自定义网关或第三方兼容端点
通常更适合显式构造模型实例。
4.4 OpenAI-compatible 的真实边界
ChatOpenAI 可以连接许多声称兼容 OpenAI API 的端点,但这个类主要面向 OpenAI
官方 API 规范。“普通 Chat 可用”不等于“全部能力兼容”。第三方端点必须分别验证
Tool Calling、强制 Tool Choice、Structured Output、Streaming、Multimodality、Usage Metadata
和错误格式。
本文中的部分模型名称来自所测试 OpenAI-compatible 网关的模型目录,可能是网关别名,不代表模型厂商的公开 API 存在同名模型。
5. 模型构造与运行时路由
5.1 统一的 build_model
公共配置中只保留一份默认模型定义:
DEFAULT_MODEL = "qwen3.7-plus"
DEFAULT_FAST_MODEL = "qwen3.6-flash"
DEFAULT_GRADER_MODEL = "qwen3.7-max"
DEFAULT_VISION_MODEL = "qwen3.7-plus"
OPENAI_COMPATIBLE_PROFILE = "openai-compatible"
QWEN_COMPATIBLE_PROFILE = "qwen-compatible"
SUPPORTED_PROVIDER_PROFILES = {
OPENAI_COMPATIBLE_PROFILE,
QWEN_COMPATIBLE_PROFILE,
}
def build_model(
*,
model_env: str = "DEEPAGENT_MODEL",
temperature: float = 0,
disable_thinking: bool = False,
) -> ChatOpenAI:
defaults = {
"DEEPAGENT_MODEL": DEFAULT_MODEL,
"DEEPAGENT_FAST_MODEL": DEFAULT_FAST_MODEL,
"DEEPAGENT_GRADER_MODEL": DEFAULT_GRADER_MODEL,
"DEEPAGENT_VISION_MODEL": DEFAULT_VISION_MODEL,
}
model_name = os.getenv(model_env, defaults.get(model_env, DEFAULT_MODEL)).strip()
provider_profile = os.getenv(
"DEEPAGENT_PROVIDER_PROFILE", OPENAI_COMPATIBLE_PROFILE
).strip()
if provider_profile not in SUPPORTED_PROVIDER_PROFILES:
raise ValueError(f"Unsupported provider profile: {provider_profile}")
extra_body = (
{"enable_thinking": False}
if disable_thinking and provider_profile == QWEN_COMPATIBLE_PROFILE
else None
)
return ChatOpenAI(
model=model_name,
api_key=require_model_env("DEEPAGENT_API_KEY", "ALIBABA_API_KEY"),
base_url=require_model_env("DEEPAGENT_BASE_URL", "ALIBABA_BASE_URL"),
temperature=temperature,
timeout=90,
max_retries=2,
extra_body=extra_body,
)
.env.example、代码默认值和单元测试都使用 DEFAULT_MODEL。单元测试再通过
monkeypatch 显式注入配置,不依赖开发者本地 .env 碰巧存在某个值。
5.2 Provider 私有参数
enable_thinking 是特定 Qwen 兼容端点使用的扩展字段,不是 OpenAI 通用参数。仅根据
模型 ID 是否以 qwen 开头无法可靠判断端点是否支持它:网关可能改名模型,也可能拒绝或忽略
未知字段。
因此本文只在显式设置:
DEEPAGENT_PROVIDER_PROFILE=qwen-compatible
且调用方请求 disable_thinking=True 时才发送该字段。默认 openai-compatible
Profile 不发送任何 Qwen 私有参数。生产系统还可以将 Profile 扩展成经过验证的能力表,
而不是根据名称猜测。
5.3 立即校验必需配置
require_model_env 只检查值是否存在,不会打印密钥,也不会在导入模块时自动请求 Provider。
密钥是否真实有效,只能由显式启动的在线集成测试验证。
尽早失败可以避免 Agent 运行多轮以后,才在某个 subagent 中得到模糊的认证异常。
5.4 使用 Runtime Context 切换模型
模型路由使用应用内部别名,不接受任意 Base URL 或模型 ID:
@dataclass(frozen=True)
class ModelContext:
profile: Literal["primary", "grader"] = "primary"
def model_for_profile(
models: Mapping[str, BaseChatModel], profile: str
) -> BaseChatModel:
model = models.get(profile)
if model is None:
raise ValueError(f"Unsupported model profile: {profile}")
return model
Middleware 在每次模型调用前选择模型,而不是只在整个 Run 开始时执行一次:
@wrap_model_call
def select_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
context = request.runtime.context
profile = context.profile if context else "primary"
model = model_for_profile(models, profile)
return handler(request.override(model=model))
Runtime Context 不会自动追加到 messages,但 Middleware、工具和 subagent 可以通过 Runtime
读取它。它适合保存用户 ID、租户 ID、模型路由等单次运行的不可变元数据,不适合替代
需要进入模型语义上下文的业务说明。
6. 创建第一个 Deep Agents Agent
6.1 定义确定性工具
@tool
def get_weather(city: str) -> str:
"""Return deterministic demo weather for a city."""
return f"{city}:晴,25℃(教学示例数据)"
这个工具故意不调用真实天气 API,从而把模型接入问题与第三方天气服务隔离。
“教学示例数据”是固定验证标记;在线测试不只检查最终文本,还同时断言:
- AIMessage.tool_calls 中存在 get_weather;
- 存在与该调用对应的 ToolMessage;
- Tool Result 和最终回答都包含固定验证标记。
6.2 调用 create_deep_agent
def build_agent(*, model=None):
return create_deep_agent(
model=model or build_model(),
tools=[get_weather],
system_prompt="你是一个严谨的中文助手;天气数据必须调用工具获得。",
)
create_deep_agent 返回编译后的 LangGraph,不是一次模型响应。在本文的
deepagents==0.6.12 中,它默认装配 Todo、Filesystem、通用 subagent 和
Summarization 等核心 Middleware;具体工具可见性与 Prompt 还可能受 Harness Profile 影响。
6.3 Tool Calling 请求链路

一次成功请求通常经过:
- 用户消息进入 State;
- Middleware 组装 Prompt、Context 和工具 Schema;
- 模型返回 get_weather 的结构化 Tool Call;
- LangGraph 执行 Python 工具;
- Tool Result 以 ToolMessage 写回 State;
- Agent Loop 再次调用模型;
- 模型生成最终 AIMessage,或继续发起下一个 Tool Call。
因此这是循环,而不是固定的“模型→工具→答案”一次性直线。
6.4 使用模块方式运行
cd source/_posts/deepagent/examples
.venv_deepagent/bin/python -m p02_quickstart.quickstart
使用 python -m 时,examples 是包搜索根目录,p02_quickstart 和 shared 的导入行为与
Pytest 保持一致。直接运行 python p02_quickstart/quickstart.py 可能导致兄弟包 shared
无法导入。
7. 测试、在线验证与故障排查
7.1 测试分层
| 测试层 | 主要检查 | 是否访问网络 |
|---|---|---|
| Compileall / Ruff | 语法和静态质量 | 否 |
| 工具单元测试 | 确定性输入输出 | 否 |
| 模型对象构造 | 默认值、Provider Profile、超时与重试 | 否,但需要 Fixture 占位配置 |
| Agent 图装配 | model/tools 节点、输入 Schema、Context Schema | 否,但需要 Fixture 占位配置 |
| Provider 能力探测 | Chat、Tool、Structured、Stream、Vision | 是 |
| Tool Calling E2E | Tool Call、ToolMessage、最终回答 | 是 |
现在的 P02 测试会显式注入默认模型,检查 Provider 私有字段只会在显式 Profile 下出现,
并验证非法模型 Profile 会返回可读错误。Agent 装配测试不再只检查 name,还会确认:
assert callable(agent.invoke)
assert {"model", "tools"}.issubset(agent.get_graph().nodes)
assert agent.get_input_schema().__name__ == "LangGraphInput"
assert switching_agent.context_schema is ModelContext
运行命令:
.venv_deepagent/bin/python -m pytest p02_quickstart -q
.venv_deepagent/bin/ruff check shared/config.py p02_quickstart conftest.py
当前结果为 6 passed和 All checks passed!。
7.2 真实 Quickstart 结果
在 Endpoint A 上使用网关目录中的 qwen3.7-plus 运行 Quickstart。
模型确实产生了 Tool Call,不是程序把工具结果预先拼入 Prompt:
{
"scene": "p02_quickstart_weather",
"status": "passed",
"model": "qwen3.7-plus",
"latency_ms": 9682,
"tool_calls": [
{"name": "get_weather", "args": {"city": "上海"}}
],
"tool_results": [
{
"name": "get_weather",
"status": "success",
"content": "上海:晴,25℃(教学示例数据)"
}
],
"usage": {
"input_tokens": 12585,
"output_tokens": 118,
"total_tokens": 12703
},
"final_answer": "上海天气查询结果:晴,25℃(教学示例数据)"
}
输入 Token 明显高于一句用户问题,是因为 Harness 还要提供文件工具、Todo、subagent、
Middleware 指令和工具 Schema。评估 Agent 成本时,不能只计算用户消息长度。
7.3 本文测试环境中的兼容性结果
下面是作者在特定端点、网关模型别名和本文锁定 SDK 版本下的实测,不是模型厂商对所有环境的兼容承诺。
| 端点 | 端点类型 | 模型 ID | 别名边界 |
|---|---|---|---|
| Endpoint A | 第三方 OpenAI-compatible 网关 | qwen3.7-plus、qwen3.7-max、qwen3.6-flash、gpt-5.6-luna 等 | 可能为网关别名 |
| Endpoint B | 第三方 OpenAI-compatible 网关 | gpt-5.5 | 可能为网关别名 |
Endpoint A 的 /models 返回 132 个 ID;Endpoint B 返回 11 个 ID。主文只保留
选型结论:Endpoint A 选择 qwen3.7-plus 作为主模型、qwen3.6-flash
作为快速模型、qwen3.7-max 作为 Grader。Endpoint B 的 gpt-5.5 通过了
Chat、Tool Calling、Pydantic 结构化输出和 Streaming 探测。
展开查看保存的能力摘要
| 模型别名 | Chat | Tool | Structured | Stream | Vision |
|---|---|---|---|---|---|
| qwen3.7-plus | 通过 | 通过 | 通过 | 通过 | 通过 |
| qwen3.7-max | 通过 | 通过 | 通过 | 通过 | 未作为视觉候选 |
| qwen3.6-flash | 通过 | 通过 | 字段名不稳定 | 通过 | 未测试 |
| qwen3.5-flash | 通过 | 通过 | 字段名不稳定 | 通过 | 未测试 |
| gpt-5.6-luna | 通过 | 当时请求组合未通过 | 通过 | 通过 | 未测试 |
| gpt-5.5 | 通过 | 通过 | 通过 | 通过 | 未测试 |
在 Endpoint A 当时的特定 Qwen 模型与网关组合中,开启思考模式时强制
tool_choice 的请求失败。这只是端点级实测,不代表所有 Qwen 模型、所有阿里云端点或所有
网关。当时保存的脱敏结果没有记录原始 HTTP 状态码和错误类型,因此本文不伪造这两项数据,
也不再将该结果概括为“Qwen 思考模式不支持强制 Tool Choice”。后续探测应同时保存
HTTP 状态码、SDK 异常类型和 Request ID 的脱敏摘要。
当时环境版本为 deepagents==0.6.12、langchain-openai==1.3.5、
openai==2.45.0 和 langchain-core==1.4.9。
7.4 常见错误与验收清单
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| Missing DEEPAGENT_API_KEY | 没有真实配置,且不在 Pytest Fixture 中 | 本地在 .env 配置;离线测试由 Fixture 注入占位值 |
| Unsupported provider profile | Profile 不在白名单 | 使用 openai-compatible 或经过验证的 qwen-compatible |
| 401 | Key 失效或端点不匹配 | 更新凭据,不对认证失败无限重试 |
| 404 / model not found | 模型 ID 不存在 | 以当前端点 /models 为准 |
| 429 | 并发或 Token 限额 | 降低并发,使用有界退避 |
| Agent 不调用工具 | Tool Schema、Prompt 或模型能力问题 | 单独探测 Tool Calling,检查结构化 Tool Call |
| 普通 Chat 成功但 Structured 失败 | 兼容端点只实现部分能力 | 逐项探测,不根据 Chat 成功推断全部兼容 |
| No module named shared | 直接按文件路径运行 | 在 examples 根目录使用 python -m |
进入下一篇前,至少确认:
- Python 3.12 虚拟环境可用;
- 已提交锁文件,uv pip check 通过;
- .env 没有进入 Git;
- 默认模型在代码、模板和测试中一致;
- Provider 私有参数由显式 Profile 控制;
- 离线测试只使用占位配置;
- 在线验收确认了 Tool Call、ToolMessage 和最终回答;
- 日志和 Trace 中不含密钥。
8. 总结
搭建 Deep Agents 环境的目标,是把 Python、依赖、配置、Provider 能力与 Agent 行为分层验证。
本文完成了四件关键事情:
- 使用 Python 3.12 和独立虚拟环境隔离系列依赖;
- 使用 requirements.in 表达直接依赖意图,使用已提交锁文件复现当前解析结果;
- 通过显式 ChatOpenAI 实例连接第三方兼容端点,并用 Provider Profile 管理私有参数;
- 将工具、模型对象、Agent 图装配和在线 Tool Calling 分层测试。
OpenAI-compatible 只是一个起点,不是兼容性结论。只有经过 Chat、Tool Calling、Structured
Output、Streaming 和多模态的逐项验证,一个模型与端点组合才能进入对应的 Agent 场景。