本篇速览

  • 本地大模型服务的"OpenAI 兼容"通常指核心请求与响应结构对齐,不等于所有可选参数都生效。
  • "兼容"至少分四层:传输层、字段层、语义层、行为层。多数问题出在语义层与行为层,而这两层最容易被忽略。
  • 最危险的不是报错,而是静默忽略——参数写了、请求成功、但行为没变,问题会拖到很后面才暴露。
  • 推荐的接入策略:先只用 model + messages 跑通,再按需加参数,每加一个验一个,把结果沉淀成一张兼容度矩阵。
  • 端侧实现与云端实现的差异集中在三处:并发与排队、上下文上限、不支持字段的处理方式(报错还是静默忽略)。

一、一句话结论

1.1 结论本身

兼容的是主干协议,不是全部细节。请求侧的 model、messages、stream,响应侧的 choices、finish_reason 这些主干字段通常没问题;而 temperature、top_p、stop、presence_penalty 这类可选参数是否真正生效,需要逐个验证,不能假设。

1.2 "兼容"其实分四层

把"兼容"这个词拆开,能少走很多弯路。它至少包含四个层级:

层级含义不兼容时的表现
传输层HTTP 方法、路径、鉴权头、内容类型是否对得上连不上、404、415
字段层请求/响应的字段名与结构是否一致解析失败、取不到值
语义层同一个字段,两端的含义与取值范围是否一致不报错,但行为不符预期
行为层流式节奏、截断规则、错误处理等动态行为是否一致偶发异常,难复现

多数人只验证前两层——能连上、能解析出内容,就认为"兼容了"。真正让人踩坑的是第三、四层:字段存在且被接受,但语义或行为不同。比如某个参数被接受却不生效,就是语义层不兼容;比如流式的分片节奏不同导致前端卡顿,就是行为层差异。

1.3 为什么"静默忽略"最危险

三种不兼容的后果,危害程度差别很大:

  • 直接报错:最好处理,立刻知道不对。
  • 语义不同:中等危害,表现为"结果怪怪的",需要对比才能发现。
  • 静默忽略:最危险。请求返回 200、字段被接受、但完全不起作用。你会以为参数设了,实际上没有。这类问题往往到了联调后期甚至上线后才暴露,而归因时很难想到"是那个参数根本没生效"。

所以字段验证的核心目标,就是把静默忽略找出来。


二、背景:为什么要在意这件事

2.1 改造场景回顾

把小智的 LLM 后端从云端改到本机(见 A 类篇),改动只是把 base_url 指向本机。这个改动之所以能成立,前提就是本地服务真的会说那套协议。

问题的微妙之处在于:这个改动通常能一次性跑通。你改完地址、重启、对话,它大概率就能说话了。于是很容易产生一种错觉——“既然能对话,说明完全兼容”。而实际上,能对话只能证明传输层和最核心的字段层是通的,语义层与行为层的情况完全未知。

2.2 同一个接口,两种实现

同样自称"OpenAI 兼容",不同实现的差异可能很大:

  • 云端服务:字段支持完整,行为经过长期打磨,文档与实现高度一致。
  • 端侧服务:优先保证主干可用,可选参数按实现情况取舍,且文档未必逐条列出支持范围。

这不是端侧实现的缺陷,而是工程取舍——端侧要把资源用在推理效率上。但作为使用者,你必须知道这个差异存在,并用实测去确认边界,而不是假定两端行为一致。

2.3 三个常见误区

误区一:文档没写的就是不重要。 恰恰相反,文档没写的字段往往是最需要验证的——因为它的行为未知。

误区二:能跑通就等于都支持。 如前所述,跑通只覆盖前两层。

误区三:一次测过就永远有效。 版本升级后,字段支持情况可能变化。兼容度矩阵要跟着版本走,不是测一次管一辈子。


2.4 一个具体的静默忽略案例

设想这样一个场景:为了让回答更稳定,你在请求里加了 temperature: 0.0,期望同一个问题每次得到同样的回答。上线后用户反馈"同一个问题答案不一样"。你去查代码,参数确实传了;查日志,请求返回 200;查响应,内容也正常。看起来一切都对,行为却不符预期。

这就是典型的静默忽略:服务端接受了 temperature 字段,却没有让它参与采样。因为不报错,问题不会在开发阶段暴露,只会在用户侧以"答案不稳定"的形式浮现——而归因时,你很难第一时间想到"是那个参数根本没生效"。

怎么破?用第 7.3 节的极端值法:设 temperature=0 连问十次,看输出是否完全一致。若仍有差异,就说明该参数没生效,此时改用别的手段——在系统提示里约束输出格式、或在调用方对结果做缓存。

这个案例说明两件事:一是**“不报错"不等于"生效了”;二是判据必须先于结论**——先想清楚"生效应该长什么样",再去测,而不是先看输出再倒推结论。后者很容易被主观期望带偏。

三、环境与准备

3.1 环境清单

  • 本地大模型服务已启动,监听本机端口(文中以 127.0.0.1:8888 为例)。
  • 已确认模型可加载、能正常出 token(先做冒烟,再做字段测试)。
  • 记录服务版本与模型名——这两项决定了兼容度的基线。

3.2 冒烟验证

在测字段之前,先确认服务本身是健康的:

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen2.5-0.5b-instruct","messages":[{"role":"user","content":"你好"}]}'

能返回带 choices 的 JSON,说明传输层与字段层是通的。这一步通过之后,才进入逐字段验证。

3.3 测试工具怎么选

三种工具各有适用场景:

  • curl:最快,适合验证"通不通"和看原始响应结构。缺点是写复杂请求麻烦。
  • Python requests:适合做批量、可重复的实验,便于自动化与统计。
  • 官方 SDK:最贴近真实业务调用方式,但它可能自带默认值,会掩盖"不传这个参数会怎样"的事实。做兼容度测试时,建议先用裸 HTTP,再用 SDK 复验。

一个实用建议:先用最原始的方式测(curl),再用 SDK 复验。因为 SDK 会填默认值、做容错,可能让你误以为某个行为是服务端能力,其实是 SDK 补的。


3.4 请求头:容易被忽略的传输层

传输层的兼容常被跳过,但它决定了"能不能连上"这件最基本的事。两个要点:

一是 Content-Type 必须显式设置。发 JSON 却不声明内容类型,服务端可能按别的方式解析,表现为 415 或解析失败:

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer not-needed" \
  -d '{"model":"<模型名>","messages":[{"role":"user","content":"你好"}]}'

二是鉴权头。本地服务通常不校验密钥,但协议要求这个头,所以给个占位即可(如 not-needed)。要注意:不校验不等于不需要传——某些客户端库会因为没有这个头而直接报错,那是客户端在校验,不是服务端。这个区别很重要,否则你会去服务端找根本不存在的问题。

传输层的排查很简单:连不上看地址与端口,415 看 Content-Type,401 看鉴权头。这三类占传输层问题的绝大多数。

四、请求结构:主干与可选字段

4.1 三个主干字段

最小可用的请求只有三个字段:

字段是否必填作用
model是指定模型;填错通常直接报错
messages是消息列表,对话的全部上下文
stream否是否流式返回;缺省通常为非流式

这三个字段是被支持程度最高的部分,也是接入时应该首先验证的部分。

4.2 messages 的三种角色与常见误用

messages 是一个数组,每项含 role 与 content:

{"messages": [
  {"role": "system",    "content": "你是一个简洁的助手。"},
  {"role": "user",      "content": "介绍一下你自己。"},
  {"role": "assistant", "content": "我是本地运行的助手。"},
  {"role": "user",      "content": "你能做什么?"}
]}

三种角色的语义:system 设定行为基调,user 是用户输入,assistant 是模型已生成的回复(用于多轮时还原上下文)。

常见误用有三个:

  1. 把 system 当 user 用:把指令写在 user 里,效果通常弱于写在 system 里。
  2. 多轮时漏带 assistant:只把用户说的话串起来,不带模型的历史回复。模型看到的上下文就不完整,表现为"记性差"。
  3. system 写得太长:端侧上下文本来就紧,过长的 system 会挤压实际对话空间。

4.3 可选参数一览

参数含义常见取值备注
temperature采样温度,越高越随机0~2端侧是否生效需实测
top_p核采样阈值0~1与 temperature 常二选一
max_tokens输出长度上限整数影响截断,通常生效
stop停止词字符串或数组是否生效需实测
presence_penalty主题新鲜度惩罚-2~2端侧常被忽略
frequency_penalty重复惩罚-2~2端侧常被忽略
n返回几条候选整数端侧多为 1
user请求标识字符串多用于审计

这张表的用法不是"记住它",而是作为测试清单——每接一个新版本,就照着过一遍。

4.4 多轮对话的 token 累积机制

这是个必须理解的机制:服务端是无状态的,上下文完全由调用方每次完整带上。

也就是说,第二轮请求要把第一轮的用户输入和模型回复都放进 messages 再发一遍;第三轮要把前三轮的都带上。随着轮次增加,messages 越来越长,占用的 token 也越来越多,直到撞上上下文上限。

由此产生两个工程约束:一是历史管理是调用方的责任,不是服务端的;二是必须做截断或摘要,否则迟早超限。这两点在端侧尤其关键,因为端侧的上下文上限通常比云端紧得多。


五、响应结构:每个字段怎么用

5.1 非流式响应的完整字段

{
  "id": "chatcmpl-local-001",
  "object": "chat.completion",
  "created": 1717000000,
  "model": "qwen2.5-0.5b-instruct",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "……"},
      "finish_reason": "stop"
    }
  ],
  "usage": {"prompt_tokens": 24, "completion_tokens": 57, "total_tokens": 81}
}

取内容的标准路径是 choices[0].message.content。注意 choices 是数组——即使你只请求一条,也要取 [0]。

5.2 finish_reason 的取值要盯住

取值含义该做什么
stop自然结束正常
length被长度截断调大输出上限,或缩短输入历史

finish_reason 是判断生成是否完整的关键。如果你的输出总在中途断掉,先别怪模型——看一眼这个字段,它往往已经给出了答案:要么输出上限设小了,要么上下文快满了。这比反复重试有效得多。

5.3 usage 用来估算上下文占用

usage 里的三个数字各有用途:

  • prompt_tokens:本次请求实际消耗的上下文长度(含历史)。
  • completion_tokens:本次生成了多少。
  • total_tokens:两者之和。

它的实际价值在于估算上下文占用。当你发现多轮对话越来越慢或突然报错,看一下 prompt 的 token 数,通常就能确认是不是历史累积逼近上限。把每轮的 token 数记进日志,定位这类问题会快很多——这是把"玄学"变成数据的典型例子。

5.4 流式响应的 delta 结构

流式时,每个片段的结构与非流式不同:

{"choices":[{"index":0,"delta":{"content":"你"},"finish_reason":null}]}

关键差异有三点:内容字段从 message 变成 delta;delta 里通常是增量而非完整内容;最后一个片段的 finish_reason 才有值,前面的多为 null。

非流式与流式混用是最常见的解析错误来源——比如用 message.content 去读流式响应,会取到空值。


六、流式 SSE 的完整解析

6.1 SSE 是什么

流式返回用的是 SSE(Server-Sent Events):服务端保持连接,不断下推文本片段,每个片段以 data: 开头,以 [DONE] 表示结束。它不是一次性返回完整 JSON,而是一串事件。

理解这一点很重要:你不能用"读完整响应再解析 JSON"的方式处理流式,必须逐行读、逐行解析。

6.2 完整解析代码

import requests, json

url = "http://127.0.0.1:8888/v1/chat/completions"
payload = {
    "model": "qwen2.5-0.5b-instruct",
    "messages": [{"role": "user", "content": "讲一个三句话的短故事。"}],
    "stream": True,
}

with requests.post(url, json=payload, stream=True, timeout=120) as r:
    for line in r.iter_lines(decode_unicode=True):
        if not line or not line.startswith("data:"):
            continue                      # 跳过空行与心跳
        data = line[len("data:"):].strip()
        if data == "[DONE]":
            break                         # 结束标记
        try:
            delta = json.loads(data)["choices"][0]["delta"].get("content", "")
        except (ValueError, KeyError, IndexError):
            continue                      # 容错:跳过异常片段
        print(delta, end="", flush=True)  # 边收边显示
print()

三处细节值得注意:stream=True 让连接保持;对 data: 之外的内容(空行、心跳)要跳过;解析要做容错,避免一个异常片段中断整个流。

6.3 边界情况

  • 心跳行:服务端可能插入空行或注释维持连接,要能跳过。
  • [DONE] 之后:理论上不再有内容,但客户端仍应能安全处理后续数据。
  • 中途出错:可能推入错误信息而非内容片段,容错逻辑要能区分。
  • 连接中断:既没有 [DONE] 也没有报错时,客户端要有超时兜底,不能无限等待。

6.4 流式在前端怎么呈现

解析出增量之后,前端呈现也有讲究。核心是边收边渲染:每收到一个增量就追加到界面,而不是等全部收完。这就是"打字机"效果的来源,它显著改善等待体验——用户看到字在出,感知到的等待时间比干等短得多。

三个细节值得注意:

一是增量拼接要注意编码。逐块拼接时若恰好在字节边界截断,可能出现乱码;按字符而非字节累积更安全。二是渲染要节流。每收一个字就刷新一次界面,高频刷新会拖慢页面,按时间间隔批量刷新更稳。三是要正确处理结束状态:收到 [DONE] 或连接正常关闭后,把界面状态置为完成并允许后续输入,否则用户会以为还在生成中。

七、最小验证实验:逐字段实测

7.1 实验设计原则

三个原则,决定了测试结果可不可信:

  1. 单一变量:一次只改一个参数,否则无法归因。
  2. 可重复:固定问题、固定轮次,结果能复现。
  3. 有判据:事先想清楚"什么结果算生效",而不是看输出后主观判断。

第三条最容易被跳过,却最关键。没有判据的测试,等于没测。

7.2 逐字段测试表

字段测什么判据(事先定好)
model填错时是否报错返回错误=生效
messages 多轮是否记得前文能引用上一轮=生效
stream是否逐块返回分多次收到=生效
temperature两次同问是否更随机多次采样输出差异变大=生效
max_tokens是否被截断出现 finish_reason=length =生效
stop遇停止词是否停提前结束=生效
presence_penalty是否抑制重复主题与不设置时有可测差异=生效

7.3 怎么判断"生效"

几个实用判据设计方法:

  • 二分对比法:同一问题,A 组设参数、B 组不设,各跑若干次,比较输出差异。差异稳定存在才算生效。
  • 极端值法:把参数设到极端(如 temperature=0),看输出是否变得确定。若设与不设毫无区别,大概率是没生效。
  • 错误法:故意填非法值,看是否报错。报错说明字段被解析了;完全没反应说明可能被忽略。

注意统计意义:单次输出有随机性,尤其是涉及采样的参数。至少跑数次、最好十次以上,看趋势而不是单次结果。

7.4 沉淀成兼容度矩阵

把结果记成一张表,接入时不用再猜:

字段是否支持判据备注
model支持填错报错—
messages支持多轮可引用—
stream支持分块返回—
temperature待测—需实测
stop待测—需实测

这张表要跟着版本走:每次升级服务或换模型,重新过一遍。它是接入工作里投入产出比很高的一项产出。


7.5 把逐字段测试写成脚本

手工测一遍容易,难的是每次升级都测一遍。把它写成脚本,成本就降下来了:

# 逐字段测试骨架:单一变量 + 多次采样 + 可计算判据
import requests

URL = "http://127.0.0.1:8888/v1/chat/completions"
BASE = {"model": "<模型名>", "messages": [{"role": "user", "content": "讲一个短故事"}]}

def run(payload, n=10):
    outs = []
    for _ in range(n):
        r = requests.post(URL, json=payload, timeout=120)
        outs.append(r.json()["choices"][0]["message"]["content"])
    return outs

def diversity(outs):
    return round(len(set(outs)) / len(outs), 2)   # 不同结果占比,粗略衡量随机性

def test_temperature():
    a = run({**BASE, "temperature": 0.0})   # 对照组:应更稳定
    b = run({**BASE, "temperature": 1.5})   # 实验组:应更发散
    return {"temp=0 多样性": diversity(a), "temp=1.5 多样性": diversity(b)}

关键是判据要可计算——用"多次输出中不同结果的比例"来衡量随机性,而不是靠人眼看。若两组数值接近,说明该参数大概率未生效。

这类脚本的价值在于可回归:纳入版本升级的验证流程后,兼容度的变化就能被自动发现,而不是等到业务出问题才察觉。

八、参数边界与异常取值

8.1 temperature 与 top_p

temperature 控制采样随机性:越低输出越确定,越高越发散。top_p 是核采样阈值,从概率累加的角度截断候选集。

实践中通常二选一调节,同时调容易互相干扰、难以归因。若你想让输出更稳定,优先用低 temperature。

需要提醒的是:这两个参数在端侧是否真正生效,必须用第七节的方法实测。若发现不生效,就改用调用方能控制的手段——比如在系统提示里明确要求"回答简洁、不要发散"。

8.2 max_tokens 与截断

max_tokens 限制本次生成的最大长度。它通常是最可靠的字段之一,但要注意两点:

  • 它不减少上下文占用,只限制输出。
  • 触发后 finish_reason 为 length,输出会在中途被切断——句子不完整。

所以合理做法是:既设 max_tokens 兜底,又在系统提示里要求简洁,双保险。

8.3 异常取值会发生什么

填写超出范围的取值(如负数、超大值、类型错误)时,不同实现的处理不同:有的返回 400 并给出错误信息,有的静默截断到合法范围,有的直接忽略。

测试异常取值的意义在于:确认服务的容错策略。如果你的代码会传动态计算出的参数值,就必须知道越界时会发生什么,否则线上会出现难以解释的行为。


九、错误响应与状态码

9.1 常见状态码含义

状态码含义常见诱因
400请求体有问题字段缺失、类型错误
401鉴权失败密钥不对(本地服务通常不校验)
404路径不存在少了 /v1 之类的前缀
422语义无法处理参数合法但组合不合理
429限流并发过高
500服务端内部错误模型、内存、推理异常

看状态码是分诊的第一步:4xx 通常是你的问题,5xx 通常是服务端的问题。这个区分能立刻把排查方向砍掉一半。

9.2 错误响应的结构

错误响应通常包含一个描述性字段(如 error 对象,含 message、type、code)。排查时要把这个信息完整记进日志——很多"莫名其妙的失败",答案就写在服务返回的错误信息里,只是没被记录下来。


9.3 限流与排队:429 与变慢的区别

端侧服务在压力下的两种表现必须区分开,因为处理方式完全相反:

  • 主动限流:返回 429,明确告诉你"太多了"。调用方应当退避重试——等待一段时间再试。
  • 被动排队:不报错,只是每个请求都变慢。原因是算力被分摊了。此时应当降低并发,减少同时发起的请求数。

如果把排队误判为限流而去重试,只会让队列更长、情况更糟——这是很典型的"越修越坏"。

判断方法很简单:看响应是明确的状态码,还是单纯的耗时上升。前者是限流,后者是排队。

十、与云端实现的差异(端侧特有)

10.1 并发与排队

云端服务可以弹性扩容,端侧服务只有一个实例、一份算力。并发上来后,请求会排队——表现为单个请求变慢,而不是报错。

工程含义:端侧调用方要限制并发,并对"变慢"有预期。若你的业务允许,把请求串行化往往比并发更快、更稳。

10.2 上下文上限更紧

端侧的上下文长度受内存限制,通常比云端紧得多。这带来两个直接后果:历史管理必须更激进(更早截断或摘要);长文档类任务在端侧要分段处理,而不是整体塞进去。

10.3 不支持字段的处理方式

这是最需要实测确认的一点:不支持的字段,是报错还是静默忽略?

两种行为对调用方的影响完全不同。报错的话你能立刻发现;静默忽略的话,你会一直以为参数生效了。测试方法很简单:故意传一个确定的非法或冷门字段,看服务返回什么。


十一、健壮客户端封装

11.1 超时与重试

import requests

def call_llm(payload, timeout=120, retries=2):
    for i in range(retries + 1):
        try:
            r = requests.post(URL, json=payload, timeout=timeout)
            if r.status_code < 500:      # 4xx 不重试,是自己的问题
                return r.json()
        except requests.exceptions.RequestException:
            pass
        if i == retries:
            raise
    return None

要点:4xx 不重试(改请求才有用),5xx 与网络异常可重试(有限次、带退避)。无限重试只会雪上加霜。

11.2 流式与非流式的统一接口

建议封装成同一个函数,用参数控制是否流式,内部处理两种解析方式的差异。这样业务层不必关心底层是流式还是非流式。

11.3 历史管理与截断

def build_messages(history, user_input, system_prompt, max_turns=10):
    msgs = [{"role": "system", "content": system_prompt}]
    recent = history[-max_turns:]        # 只保留最近若干轮
    msgs.extend(recent)
    msgs.append({"role": "user", "content": user_input})
    return msgs

核心是控制长度:只保留最近若干轮,并在超限时做摘要。这一层必须在调用方实现,因为服务端不替你管。

11.4 把兼容度判断也收进封装

一个进阶建议:在封装层记录每次请求的 finish_reason 与 usage,当出现 length 或 token 数逼近上限时主动告警。这样"上下文快满了"这件事会在出问题前被发现,而不是等到报错。


11.5 日志与观测:把关键字段记下来

客户端封装里应该记录四样东西:请求的关键字段(模型、是否流式、参数)、耗时、响应的 finish_reason、以及 usage 的三个 token 数。

用途分别是:参数用于复现问题;耗时用于性能观测与告警;finish_reason 用于发现截断;usage 用于发现上下文逼近上限。

后两项尤其有价值——它们能让问题在变成故障之前被看到。比如当 usage 里的 prompt token 数持续上升,就说明历史管理有问题,迟早会超限;当 finish_reason 频繁出现 length,就说明输出上限或输入长度需要调整。把这两个字段从"响应里的一行"变成"监控里的一条曲线",你就从被动排障转向了主动预防。

十二、问题与解决方案

参数写了没效果:先确认该参数是否在支持范围内(回到第七节的实测矩阵);若确实不支持,换用调用方能控制的手段达成目的。

输出被截断:看 finish_reason。若为 length,调大输出上限或缩短历史。

多轮不记得前文:服务端无状态,历史必须由调用方每次完整带上,并控制在上下文长度内。

流式收到一半断开:常见原因是超时(客户端设太短)、服务端中断、或网络抖动。排查顺序:先看服务端日志,再放宽超时重测;跨机场景先同机复现以区分网络问题。客户端要能容忍不完整结果,并给出明确失败提示。

偶发的解析失败:给解析逻辑加容错(见 6.2),跳过异常片段而不是让整个流中断。


响应很慢但没超时:多半是排队或模型偏大,而不是网络。按第十节的思路区分——并发上来才变慢是排队,单请求就慢是模型或上下文问题。对策分别是限并发、以及调小模型与上下文。

同一请求两次结果不一样:这是采样的正常表现,不是缺陷。若需要稳定输出,降低随机性参数,或在系统提示里约束输出格式。要注意"结果不一致"和"结果错误"是两回事,别混为一谈——前者可能只是正常的多样性,后者才是要修的问题。

中文输出偶现乱码:优先怀疑流式拼接时按字节截断(见 6.4),或请求与响应的字符集设置不统一。非流式场景若出现,则检查两端的编码声明是否一致。

加了参数之后输出反而变差:先确认该参数是否被正确支持(静默忽略除外,还要考虑语义差异——比如同一个参数名,两端的取值范围或默认行为不同)。这种情况属于第 1.2 节说的语义层不兼容,排查时要把"是否支持"和"含义是否一致"分开验证。

十三、边界与不适用

  • 兼容度随版本变化:本文的测试方法是稳定的,但"哪些字段支持"这个结论只对当前版本成立。
  • 端侧能力有限:不要期待端侧服务支持云端的所有可选参数,主干可用即可满足绝大多数改造场景。
  • 统计判断有随机性:涉及采样的参数,单次结果不足以判定,要看多次趋势。
  • 不要跨配置比较结论:换了模型或版本,兼容度矩阵要重测。

十四、总结与可带走物

要带走的判断方法只有一句:"兼容"永远要被验证,不能被假设。主干字段通常没问题,可选参数则各有各的情况——花十几分钟把字段过一遍,能省掉后面几小时的困惑。

可复用的产出有三样:

  1. 逐字段测试表(第七节),照着跑即可。
  2. 兼容度矩阵(7.4),跟着版本维护。
  3. 健壮客户端封装(第十一节),把超时、重试、流式解析、历史管理一次收口。

这三样加起来,就把"能不能接"这件事,从凭运气变成了有流程、有依据的工程动作。

还需要提醒一句关于版本演进的预期:本地推理服务是活跃演进的,今天不支持的字段,下个版本可能就支持了;反过来,今天跑通的写法也可能在某个版本后行为变化。所以第七节那张兼容度矩阵必须标注服务版本,每次升级后复测一遍。把兼容当成一次性的验收结论,是这类改造里最常见的长期隐患——问题往往在几个月后升级时才暴露,而那时已经没人记得当初验过什么。

14.1 接入检查清单

  • 传输层已验证:地址、端口、Content-Type、鉴权头
  • 主干字段已跑通:model + messages(按需加 stream)
  • 可选参数已逐个实测,结果已记入兼容度矩阵
  • finish_reason 与 usage 已纳入日志与观测
  • 客户端已封装:超时、重试(4xx 不重试)、流式解析容错
  • 历史管理与截断已在调用方实现
  • 并发已限制,并对"排队变慢"有预期
  • 兼容度矩阵已标注服务版本,升级后需复测

本文涉及的接口字段与结构为 OpenAI 兼容协议的通用约定,具体支持范围以本地服务当前版本文档为准;文中命令与代码可直接执行,输出示例为示意值,请以真机实际返回为准;兼容度矩阵中的结论需按第七节方法实测后填写,不得沿用他处结论。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐