Prhub

#1979 [agent] Add openai and anthropic adapters

原始 PR 作者 zhuzilin 合并时间 2026-05-29 19:30 文件变更 16 提交数 4 评论 0 代码增减 +2327 / -248

执行摘要

新增 OpenAI 和 Anthropic 适配器并重构 agent 模块

为了支持更多 LLM API 协议(OpenAI 和 Anthropic)作为 agent 推理的驱动,使 slime 的 agent RL 训练可以接入更广泛的生态工具,同时将适配器逻辑从示例中解耦出来,提升可维护性和可扩展性。

建议所有参与 agent RL 训练的成员阅读此 PR,尤其是 common.py 中的 AdapterChaincall_sglang_generate 设计,它为后续添加新的 adapter 提供了清晰的模式。openai.py 中的 _translate_chat_messages_responses_input_to_messages 体现了多协议消息格式转换的典型做法,值得参考。

讨论亮点

该 PR 未产生 review 讨论。

实现拆解

  1. 创建公共基模块 slime/agent/adapters/common.py,定义了协议无关的 AdapterChain 数据结构、session 生命周期管理函数(register_sessionrequest_session_id)、token 渲染函数 render_token_ids、SGLang 生成调用 call_sglang_generate 以及采样参数合并函数 _sampling_params
  2. 实现 OpenAI 适配器 slime/agent/adapters/openai.py,对外暴露 /v1/chat/completions/v1/responses 端点,负责将 OpenAI 格式的消息体通过 chat template 转为 input_ids,调用 SGLang 并记录 token 级推理轨迹。内部实现了消息扁平化 _flatten_content、工具调用标准化 _normalize_tool_call、角色映射 _translate_chat_messages 等函数。
  3. 迁移 Anthropic 适配器examples/coding_agent_rl/middleware.py 重构并搬迁至 slime/agent/adapters/anthropic.py,复用公共基类中的 Chain、session 管理、render_token_idscall_sglang_generate 等,精简了约 200 行重复代码。同时保留了对 Claude Code 多轮对话和子 agent 链的分段支持。
  4. 添加测试套件 新增 tests/test_agent_adapters.py(单元测试,覆盖消息翻译、session 提取、SSE 解析、流式响应等)和 tests/test_agent_sdk_adapters.py(集成测试,模拟 OpenAI SDK 和 Anthropic SDK 的完整工具循环)。
  5. 调整下游代码与 CI 更新 examples/coding_agent_rl/generate.pysandbox.py 中的导入路径,将 middleware 引用替换为 slime.agent.adapters.anthropic;扩充 CI 配置文件以包含新的 adapter 测试用例。
文件 模块 状态 重要度
slime/agent/adapters/openai.py OpenAI 适配器 added 9.08
slime/agent/adapters/common.py 公共基类 added 9.04
slime/agent/adapters/anthropic.py Anthropic 适配器 renamed 8.89
tests/test_agent_adapters.py 测试套件 added 7.76
tests/test_agent_sdk_adapters.py 集成测试 added 7.76
examples/coding_agent_rl/generate.py 示例脚本 modified 6.15
.github/workflows/pr-test.yml CI 配置 modified 4.82

关键符号

slime/agent/adapters/openai::_translate_chat_messages slime/agent/adapters/openai::_flatten_content slime/agent/adapters/openai::_normalize_tool_call slime/agent/adapters/common::_sampling_params slime/agent/adapters/common::call_sglang_generate slime/agent/adapters/common::register_session slime/agent/adapters/anthropic::_build_prompt slime/agent/adapters/anthropic::_select_chain

关键源码片段

slime/agent/adapters/openai.py dependency-wiring

新增的 OpenAI 适配器主文件,实现 chat/completions 和 responses 端点,核心消息转换与工具调用标准化逻辑所在。

# 代码片段:_translate_chat_messages 将 OpenAI 消息列表转换为 chat-template 兼容格式
# 处理角色映射、内容扁平化、工具调用标准化等from slime.agent.adapters.common import json_arguments as _json_arguments
from slime.agent.adapters.common import stable_hash as _hashdef _translate_chat_messages(messages: list[dict]) -> list[dict]:
    """OpenAI chat messages -> tokenizer chat-template messages."""
    translated: list[dict] = []
    for msg in messages:
        if not isinstance(msg, dict):
            continue
        role = msg.get("role")
        content = msg.get("content")
        # 将 'developer' 角色统一为 'system'
        if role == "developer":
            role = "system"
​
        # 系统 / 用户消息:扁平化 content 字段
        if role in {"system", "user"}:
            translated.append({"role": role, "content": _flatten_content(content)})
        # 工具返回消息:保留 tool_call_id
        elif role == "tool":
            tool_msg = {"role": "tool", "content": _flatten_content(content)}
            if msg.get("tool_call_id"):
                tool_msg["tool_call_id"] = msg["tool_call_id"]
            translated.append(tool_msg)
        # 助手消息:携带推理内容和工具调用
        elif role == "assistant":
            assistant: dict[str, Any] = {"role": "assistant", "content": _flatten_content(content)}
            if msg.get("reasoning_content"):
                assistant["reasoning_content"] = msg["reasoning_content"]
            tool_calls = msg.get("tool_calls") or []
            if tool_calls:
                # 标准化每个工具调用格式
                assistant["tool_calls"] = [_normalize_tool_call(c) for c in tool_calls if isinstance(c, dict)]
            translated.append(assistant)
    return translated
slime/agent/adapters/common.py dependency-wiring

适配器公共基础设施,包含 AdapterChain 数据结构、session 管理、token 渲染、采样参数合并和 SGLang 调用封装。

# 代码片段:_sampling_params 合并 session 默认参数和请求 body 中的采样参数
# 同时支持上下文长度截断保护def _sampling_params(session: Any, body: dict, *, max_token_keys: tuple[str, ...], stop_keys: tuple[str, ...]) -> dict:
    # 默认参数:禁止跳过特殊 token、禁止额外空格、不裁剪 stop
    sp: dict[str, Any] = {
        "skip_special_tokens": False,
        "spaces_between_special_tokens": False,
        "no_stop_trim": True,
        "max_new_tokens": 4096,
        **(session.sampling_defaults or {}),
    }
​
    # 从 body 中按优先顺序选取 max_new_tokens
    for key in max_token_keys:
        if body.get(key) is not None:
            sp["max_new_tokens"] = min(int(sp.get("max_new_tokens", body[key])), int(body[key]))
            break
​
    # 覆盖温度、top_p、top_k
    for src_k, dst_k in (("temperature", "temperature"), ("top_p", "top_p"), ("top_k", "top_k")):
        if src_k in body:
            sp[dst_k] = body[src_k]
​
    # 处理 stop 序列
    for key in stop_keys:
        if body.get(key):
            sp["stop"] = body[key]
            break
​
    return sp
slime/agent/adapters/anthropic.py rename-or-move

从 examples/coding_agent_rl/middleware.py 重构并搬迁过来的 Anthropic 适配器,复用公共基类,精简了大量重复代码。

# 代码片段:Session 数据结构和 _build_prompt 函数
# Session 包含主链和可能的子 agent 链,支持分段轨迹import dataclasses
from slime.agent.adapters.common import AdapterChain as Chain
from slime.agent.adapters.common import render_token_ids
from slime.agent.trajectory import TurnSegment@dataclasses.dataclass
class Session:
    main: Chain = dataclasses.field(default_factory=Chain)
    active_sub: Chain | None = None # 最多一个活跃子 agent
    pending_dispatch_id: str = ""
    sampling_defaults: dict = dataclasses.field(default_factory=dict)
    max_context_tokens: int = 0
    lock: asyncio.Lock = dataclasses.field(default_factory=asyncio.Lock)
    segments: list[TurnSegment] = dataclasses.field(default_factory=list) # 冻结的输出def _build_prompt(target: Chain, body: dict, kind: str, tok) -> list[int]:
    """根据 kind 选择替换或扩展消息,然后渲染 token id。"""
    if kind == "append":
        _extend_chat_messages(target, body)
    else:
        _replace_chat_messages(target, body)
    return render_token_ids(target, tok)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 重构后 examples/coding_agent_rl 的依赖路径发生变化,需确保现有启动脚本和配置兼容。
  2. 新的 OpenAI adapter 处理 responses 格式时对 multi-turn 工具循环的支持尚未在生产环境中验证。
  3. 测试套件依赖于 agentsanthropicopenai SDK,若环境缺少这些依赖会 skip 集成测试,可能掩盖回归。
  4. session_id 提取逻辑同时支持多种 header/body 字段,可能存在优先级冲突。

对用户:开发者现在可以通过 OpenAI 或 Anthropic 协议驱动 agent 推理,而不仅限于 Anthropic Claude Code 的专用格式。对系统:适配器代码集中到 slime/agent/adapters,降低了与示例代码的耦合,便于后续扩展新协议(如 Google Gemini)。对团队:需要了解新的模块结构,并保持 common.py 作为协议无关层的通用性。

核心路径变更 缺少生产验证 重构影响现有流程 依赖外部 SDK 测试

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论