openai-compatible-client-migration · ZH · 2026-10-18

把 LangChain agent 迁到聚合站:工具调用与流式能力过代理后还剩哪些

用 LangChain 的工具调用 agent 接聚合站的 OpenAI 兼容端点,判断哪些能力在代理层会退化:流式增量、工具调用参数拼装、多轮消息回传、usage 与错误结构。给出逐步迁移与逐模型验收的检查清单。

迁移前先明确一件事

聚合站对外提供的是 OpenAI 兼容端点。LangChain 侧的 ChatOpenAI 只需更换 baseurl 和 apikey 就能接上,代码形态基本不用改。真正需要评估的是:请求经过一层代理,被转发到 Claude、GPT、DeepSeek、Qwen、GLM、Kimi 等不同后端时,哪些行为会与直连官方 API 不一致。

答案不是"能用/不能用"的二元判断,而是逐项能力分模型标记。下面按一次完整的工具调用循环逐段看。

第一步:把端点换掉并跑通最小循环

先用 ChatOpenAI 指向聚合站,跑一个只有单个工具的最小 agent。这一步的目的是确认链路通了,而不是确认质量。

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="<聚合站里的模型标识>",
    base_url="https://<聚合站域名>/v1",
    api_key="<你的 key>",
    streaming=True,
)

要点:

  • model 用聚合站文档里给出的标识,不要沿用官方模型名去猜。
  • 第一次连接先关掉 streaming,确认非流式路径下的工具调用能返回结构化结果,再打开流式排查。
  • 用 bindtools 绑定工具,让 LangChain 走标准的 tools/toolcalls 字段,而不是自定义函数调用格式。

工具调用的参数是流式拼出来的

这是代理层最容易出问题的地方。流式响应里,工具调用的 arguments 是一段段 JSON 片段陆续到达的,客户端(这里是 LangChain 的解析器)负责累加拼装。

观察点:

  • 增量片段是否带正确的 index,能否被正确归到对应的 tool_call。
  • 结束标记是否完整,解析器能否判断参数已收齐。
  • 参数里的非 ASCII 字符(中文、emoji)在分片处被切断时是否仍能拼回合法 JSON。

如果某个后端在流式下工具调用参数拼装失败,可以先把该模型从流式 agent 路由里摘出来,改走非流式,或在 LangChain 层用 astream_events 观察事件序列定位问题。

多轮消息回传:工具结果怎么塞回去

工具执行完,结果要以 tool 角色回传给模型,并带上对应的 toolcallid。这是标准 OpenAI 消息结构,LangChain 的 agent executor 会自动处理。

代理层会动的点:

  • 消息顺序与角色映射。转发到非 OpenAI 后端时要重构成对方的消息格式,重构后 toolcallid 是否仍能对应上。
  • 并行工具调用。一次返回多个 tool_call 时,多个 tool 结果回传的顺序和配对是否被保持。
  • 长对话里的历史裁剪。如果聚合站对上下文长度有自己的处理,历史消息被截断后可能出现孤立的 tool 消息,触发后端报错。

验收方法:造一个会触发两个并行工具调用的 prompt,看第二轮请求是否被正确接受。

流式的三层:文本、工具、事件

流式能力实际上有三层,要分开验收:

  1. 文本增量。stream=True 下首 token 是否能到、增量是否连续。
  2. 工具调用增量。如上节所述,参数分片拼装。
  3. LangChain 事件流。astreamevents 能否拿到 onchatmodelstream、ontoolstart、ontoolend 等事件,前端想按步骤渲染就必须依赖这一层。

后两层依赖第一层的原始 SSE 是否符合 OpenAI 规范。如果某个后端在代理下的原始事件格式被转换过,LangChain 的解析器可能仍能读出文本,但事件粒度会变粗,步骤级 UI 就渲染不出来。

逐后端标记能力退化

建议维护一张表,不凭印象,每个模型跑同一组用例后填:

  • 非流式工具调用:通过 / 不通过
  • 流式文本增量:通过 / 不通过
  • 流式工具参数拼装:通过 / 降级 / 不通过
  • 并行工具调用:通过 / 不通过
  • 多轮 tool 消息回传:通过 / 不通过
  • usage 字段:完整 / 缺失 / 只在非流式下返回
  • 错误结构:标准 OpenAI error 体 / 包装过 / 纯文本

差异往往不在"能不能调工具",而在流式细节和 usage 上。把这张表贴在项目里,路由逻辑就有依据。

错误与重试要重新写

直连官方 API 时,你对 429、超时、上下文超长的错误码有稳定预期。过代理后多了一层,错误来源变多:

  • 上游模型返回的错误,被代理原样透传还是重新包装。
  • 代理自身的错误(key 失效、余额不足、上游暂不可用)。

LangChain 的重试逻辑基于异常类型和状态码。如果代理把所有上游错误统一成同一个状态码,max_retries 的行为就会失真。做法是在 ChatOpenAI 外再包一层自己的错误归一化,按响应体内容而不是仅凭状态码分类。

usage 与成本观察

流式响应下 usage 字段经常缺失,这是 OpenAI 兼容实现里的常见现象,不一定是代理造成的。计费按官方价 ×1.3 扣费,所以你需要能对账:

  • 非流式调用拿 usage 做基准。
  • 流式场景靠聚合站账单页核对总量。
  • 在 agent 里把每轮调用的模型标识和 token 用量记进日志,便于按模型看成本分布。

迁移检查清单

  1. 用聚合站给的模型标识替换 model,改 base_url。
  2. 关闭流式,跑通单工具循环。
  3. 打开流式,用 astream_events 观察文本、工具、事件三层。
  4. 造并行工具调用用例,验收多轮回传。
  5. 制造一次上游错误和一次本地 key 错误,检查错误结构。
  6. 对每个后端模型重复 2–5,填进能力表。
  7. 按能力表决定哪些模型走流式 agent,哪些走非流式。

工具调用和流式这两项能力,过代理后不会整体消失,但会在分片拼装、事件粒度、错误结构这些细节上按后端分化。迁移的工作量主要花在验收和路由,而不是改代码。