[控场AI]
· 11 分钟阅读· 5,581 字

LangExtract接兼容OpenAI API的避坑指南:模型路由、严格模式与静默丢块

LangExtract接兼容OpenAI API的避坑指南:模型路由、严格模式与静默丢块

LangExtract 对接 OpenAI 兼容端点时的路由陷阱、严格模式降级与静默丢块问题及修复方案。

本文基于 langextract 1.7.1 的实测,系统梳理了该库对接 OpenAI 兼容 API 时的若干隐蔽陷阱。核心发现包括:模型路由依赖 model_id 正则匹配,qwen、llama 等非 GPT 命名会被误导向 Ollama provider,必须用 ModelConfig 显式指定 provider;默认请求不带 temperature 和 max_tokens,且强制使用 json_schema 严格模式,服务端不支持时需降级为 json_object;最危险的是解析失败默认被静默丢弃,长文档中会造成漏抽而不报错,应开启 fence_output 并关闭 suppress_parse_errors。文章还量化了分块、并发与多轮对请求量的放大效应,并指出重试仅来自 OpenAI SDK 本身。

LangExtract接兼容OpenAI API的避坑指南:模型路由、严格模式与静默丢块

Google 的 LangExtract 是一个能从长文本中抽取结构化实体的 Python 库,每个抽取结果都会映射回源文本中的字符位置。你只需描述任务、给一两个示例,它就会自动分块、逐块调用模型、解析 JSON 并对齐结果。默认后端是 Gemini,但它内置了基于官方 OpenAI SDK 的 provider,理论上可以对接 vLLM、LiteLLM 或任何托管网关。

LangExtract 兼容 OpenAI API 实测

然而,当你想把 model_id 指向一个非 OpenAI 命名的模型(比如本地部署的 qwen3-coder-plus)时,问题就来了。本文基于对 langextract 1.7.1(openai 3.35.1、Python 3.13)的实测,用一个会记录所有请求、并能按需返回 400/429/503、带围栏 JSON 或截断输出的本地 mock server,还原网络上真实发生的事。作者同时声明自己开发了示例中的网关 APIClaw,因此对网关相关观点需带一定保留。

模型名决定走哪个 Provider

最直观的调用方式是这样的:

import langextract as lx

result = lx.extract(
    text_or_documents=text,
    prompt_description=prompt,
    examples=examples,
    model_id="qwen3-coder-plus",
    api_key="your-key",
    model_url="https://apiclaw.biz/v1",
)

看起来没问题,但 LangExtract 的路由是基于 model_id 的正则匹配实现的。只有以 gpt-3.5、gpt-4、gpt-5 或 o1 到 o9 开头的名字才会走 OpenAI provider。以 qwen、deepseek、llama、mistral、gemma、phi、gpt-oss 等开头的名字,会被导向 Ollama provider——哪怕你传了 model_url。

实测结果很能说明问题:

  • qwen3-coder-plus 和 gpt-oss-120b 被送进了 Ollama 客户端,向 <base_url>/v1/api/generate 发 POST,返回 Can't find Ollama qwen3-coder-plus. Try: ollama run qwen3-coder-plus。
  • 完全不匹配任何模式的 plain-model 直接报错:No provider registered for model_id='plain-model'。
  • 而 gpt-4o-mini 配合 model_url 则正常命中 /v1/chat/completions。

换句话说,上面那段“理所当然”的代码,只有当你的网关模型恰好长得像 OpenAI 模型名时才能跑通。

用 ModelConfig 强制指定 OpenAI Provider

绕过路由正则的方式是显式构造 ModelConfig:

import os
import langextract as lx

config = lx.factory.ModelConfig(
    model_id="YOUR_MODEL_ID",
    provider="OpenAILanguageModel",   # "openai" 也可以
    provider_kwargs={
        "api_key": os.environ["APICLAW_API_KEY"],
        "base_url": "https://apiclaw.biz/v1",
        "temperature": 0.0,
        "max_output_tokens": 2000,
        "max_workers": 4,
    },
)

result = lx.extract(
    text_or_documents=text,
    prompt_description="Extract every person mentioned, with their role. Use exact text.",
    examples=examples,
    config=config,
    show_progress=False,
)
for e in result.extractions:
    print(e.extraction_class, e.extraction_text, e.attributes, e.char_interval)

这样无论 model_id 叫什么,请求都稳定发往 POST /v1/chat/completions。有两个细节必须注意:

  • api_key 要显式传。 LangExtract 只有在模型 ID 包含 gpt 时才会去读环境变量 OPENAI_API_KEY。当 provider 被显式指定、名字却是 qwen3-coder-plus 时,漏传 api_key 会直接报 API key not provided.,即便环境变量已设置。
  • base_url 可以走环境变量。 不传 base_url 时,SDK 会读取 OPENAI_BASE_URL。无论哪种方式,末尾都要保留 /v1。

启用 config 时还会看到一条警告:With 'config', schema constraints are still applied via examples.——这直接关系到下一节的内容。

每个请求里到底装了什么

每个文本块都会变成一个请求,内容包含:

  • 一条 system message:You are a helpful assistant that responds in JSON format.
  • 一条 user message:包含 prompt 描述、按 Q:/A: 排列的示例及其期望 JSON、以及当前块文本;
  • n: 1;
  • response_format 类型为 json_schema,名称 langextract_extractions,带 strict: true,schema 由你的示例生成。

默认情况下请求里既没有 temperature 也没有 max_tokens,完全由服务端默认值接管。在 provider_kwargs 中显式设置后,请求会带上 temperature: 0.0 和 max_tokens: 2000。做抽取任务时建议两者都设:低温度保证一致性,输出上限则要能容纳一长串实体。

服务端不支持 json_schema 怎么办

严格的 json_schema 输出并非所有模型和兼容服务都支持。部分模型只支持 json_object,有些则什么都不支持。mock server 对 json_schema 返回 400 时,整个 extract() 直接抛出 InferenceRuntimeError: OpenAI API error: Error code: 400。

解决办法是关闭 schema 约束:

result = lx.extract(..., config=config, use_schema_constraints=False)

关闭后请求切换为 response_format: {"type": "json_object"},立刻成功。prompt 中的示例仍然会向模型展示期望的结构,只是不再有服务端强约束。

解析失败被静默丢弃,这是最该改的默认值

这是实测中最危险的一处。LangExtract 默认会吞掉每个块的解析错误。以下两种情况,运行都只打印一行日志,返回零条抽取结果且不抛异常:

  • 模型把答案包在解释文字和围栏里(先输出 Here is the result:,再跟一个 fenced json 块)。在 json_object 模式下 LangExtract 期望的是裸 JSON,于是该块被丢弃,日志为 Chunk omitted from results (parse error): Failed to parse JSON content。
  • 回复因 finish_reason: "length" 被截断,残缺 JSON 解析失败,同样被静默丢弃。

在一份 40 块的长文档里,几个被丢掉的块看起来就像是“模型在那段里什么都没找到”——这会导致你得出完全错误的抽取结论。修复方式是:

result = lx.extract(
    ...,
    config=config,
    use_schema_constraints=False,
    fence_output=True,                                  # 接受带围栏的 JSON 回复
    resolver_params={"suppress_parse_errors": False},   # 改为大声失败
)

fence_output=True 能正确解析带围栏的回复;suppress_parse_errors: False 会把静默丢弃变成可捕获的 ResolverParsingError,让你可以重试。实测中还发现,以 <think>...</think> 块开头的回复能够正常解析,所以推理模型本身并不构成问题。

分块、并发与多轮如何放大请求量

在一份 5,320 字符的文档上,实测得到的请求数量如下:

设置请求数最大并发
默认(max_char_buffer=1000)66
max_workers: 3(provider_kwargs 内)63
extraction_passes=3186
max_char_buffer=400022

每个请求都会重复携带 prompt 描述和示例,因此块越小,固定开销在请求中的占比就越高。max_workers 默认是 10,意味着长文档可能同时打出 10 个请求;如果你的套餐有每分钟限额或并发上限,务必调低。extraction_passes 会串行地把整份文档重跑一遍,三轮就是请求量和耗时同时三倍。

把 max_char_buffer 调大能减少请求数,但更长的块也意味着模型需要跟踪更多内容,在大幅提高之前最好先在自己的文档上测试召回率。

重试只来自 SDK,LangExtract 自己不重试

让 mock server 持续返回 503,SDK 默认重试两次后共发出三次请求,随后 extract() 抛出 InferenceRuntimeError。返回 429 并附带 Retry-After: 2 时,SDK 正确等待,第三次请求在约 4.6 秒后成功。LangExtract 自身没有任何重试循环,所以大规模批处理任务需要你自己在调用层包一层重试;同时记住,任何一个块失败都会让整份文档失败。

实操清单

  • 除非 model_id 以 gpt- 或 o1~o9 开头,否则用 ModelConfig(provider="OpenAILanguageModel") 显式指定 provider。
  • 在 provider_kwargs 里显式传入 api_key;环境变量密钥只对 gpt 开头的模型名生效。
  • 自己设置 temperature 和 max_output_tokens。
  • 服务端拒绝 json_schema 时,改用 use_schema_constraints=False。
  • 设置 suppress_parse_errors: False,并对爱聊天的模型加 fence_output=True,避免块被静默丢弃。
  • 根据速率限制调低 max_workers,并把 extraction_passes 当作请求量乘数来估算成本。

这些结论虽然是在 APIClaw 上验证的,但同样适用于任何 OpenAI 兼容端点——路由逻辑、schema 约束和静默丢块都是 LangExtract 客户端自身的行为,与具体网关无关。

分享:

相关推荐