Deep Agents 2. 环境搭建、模型选择与第一个 Agent


1. 本文目标与环境基线

1.1 本文要解决的问题

一个能够长期使用的 Deep Agents 开发环境,至少要回答下面几个问题:

  1. 使用哪个 Python 和 deepagents 版本;
  2. 如何隔离、锁定和验证依赖;
  3. 模型需要具备哪些能力;
  4. 如何安全保存 Provider 配置;
  5. 如何区分标准 OpenAI API 与第三方兼容端点;
  6. 如何确认模型真的产生了 Tool Call;
  7. 哪些测试需要真实凭据,哪些只需要占位配置。

本文完成后,会得到一个具有锁文件、安全配置、离线测试和真实 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 选择模型

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 请求链路

第一个 Agent 的 Tool Calling 循环

一次成功请求通常经过:

  1. 用户消息进入 State;
  2. Middleware 组装 Prompt、Context 和工具 Schema;
  3. 模型返回 get_weather 的结构化 Tool Call;
  4. LangGraph 执行 Python 工具;
  5. Tool Result 以 ToolMessage 写回 State;
  6. Agent Loop 再次调用模型;
  7. 模型生成最终 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 行为分层验证。
本文完成了四件关键事情:

  1. 使用 Python 3.12 和独立虚拟环境隔离系列依赖;
  2. 使用 requirements.in 表达直接依赖意图,使用已提交锁文件复现当前解析结果;
  3. 通过显式 ChatOpenAI 实例连接第三方兼容端点,并用 Provider Profile 管理私有参数;
  4. 将工具、模型对象、Agent 图装配和在线 Tool Calling 分层测试。

OpenAI-compatible 只是一个起点,不是兼容性结论。只有经过 Chat、Tool Calling、Structured
Output、Streaming 和多模态的逐项验证,一个模型与端点组合才能进入对应的 Agent 场景。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录