010、国内大模型API接入实践:通义、文心、GLM等 010、国内大模型API接入实践通义、文心、GLM等翻车经历先放这儿上周联调一个文档摘要功能用通义千问的qwen-plus本地测试好好的一上服务器就报InvalidApiKey。我盯着代码看了半天key明明写在环境变量里配置文件也加载了最后发现是服务器时区问题——不是时区影响签名而是我习惯在代码里print了一截key用于调试日志打到收集系统里被脱敏替换成了***再跑一轮时候环境变量被覆盖。这问题查了我四个小时后来把所有调试输出里的敏感字段全改成[REDACTED]才消停。所以说接国内大模型第一要务不是看文档是把你的调试习惯先收拾干净。很多人觉得国内大模型API不就是OpenAI套壳吗BaseURL一换模型名一改完事。真这么干你迟早被坑。通义、文心、GLM这三家虽然都兼容OpenAI的接口风格但各自在鉴权、超时、流式输出、错误码上都有“小脾气”。我先把三家最核心的差异摆出来再一个个说怎么接。先交代共同点这三家都支持HTTP调用也都提供OpenAI兼容模式。我用Python的requests库直接打原始接口不额外装SDK。为什么因为SDK版本更新飞快线上环境锁版本太痛苦而且调试时候你看不到真实请求体。用requests所有参数都在你眼皮底下出了问题抓包都省了。通义千问的接入坑在“历史消息格式”和“流式输出”。它的OpenAI兼容端点/compatible-mode/v1/chat/completions参数大体和OpenAI一样但是messages里如果带system有些模型会无视掉比如qwen-turbo。你不信我试过把系统提示词放进去让模型“只回答JSON”结果它照样给你输出花括号外的解释文字。解决办法把系统提示词拼到第一条user消息里用“请严格按照以下要求… ### 指令”这种分隔符。代码里这么写messages[# 别把system放这里qwen-turbo会假装没看见{role:user,content:f你是一个只输出JSON的助手。\n### 指令\n{system_text}\n### 用户问题\n{user_text}}]然后响应解析通义返回的content可能是字符串也可能是列表多模态时候。你要是只做文本直接判断isinstance再取。这里踩过坑我一开始直接resp[choices][0][message][content]结果有一次模型返回了多段文本列表解析直接炸。流式输出通义的stream参数设成True后返回的SSE格式里data:前缀后面不是纯JSON有时候会带个[DONE]标记有时候还会多一个空行。你按OpenAI的解析方式用line.startswith(data:)过滤没问题但要注意通义的chunk里delta可能是空的只有finish_reason。别看到空delta就直接break要判断finish_reason是不是stop这里我写错过导致流式输出最后一段话丢失。forlineinresp.iter_lines():ifnotline:continuelineline.decode(utf-8)ifnotline.startswith(data:):continuedatajson.loads(line[5:].strip())deltadata[choices][0].get(delta,{})ifnotdelta:# 这里很常见别一看到没delta就退出finishdata[choices][0].get(finish_reason)iffinishstop:breakcontinuecontentdelta.get(content)ifcontent:yieldcontent文心一言的接入最痛苦的是鉴权。它不像通义那样拿API Key直接放Header里。文心要用API Key Secret Key去换Access Token。流程是先请求https://aip.baidubce.com/oauth/2.0/token拿到access_token然后调https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions这个URL后面还要拼?access_tokenXXX。很多人第一次接以为像通义一样设置Authorization: Bearer就行结果文心根本不认返回110错误码意思就是“access_token无效”。而且这个token有效期是30天但建议你自己缓存起来因为每次调用都去换token文心接口的QPS限制会让你直接打爆。我在项目里用Redis缓存key是baidu_access_token过期时间设expires_in - 600秒留10分钟余量。defget_token():# 别每次调用都去请求token会限额cachedredis.get(baidu_access_token)ifcached:returncached resprequests.post(TOKEN_URL,data{grant_type:client_credentials,client_id:API_KEY,client_secret:SECRET_KEY}).json()tokenresp[access_token]redis.setex(baidu_access_token,resp[expires_in]-600,token)returntoken文心的另一个坑是“模型名和URL绑定”。它的OpenAI兼容端点不是统一的/v1/chat/completions而是每个模型一个URL。比如ernie-4.0-8k对应/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_proernie-3.5-8k对应completions还有ernie-speed-128k得去查文档。这个设计很反人类强烈建议封装一层API路由根据模型名动态拼URL别写死。我见过有人把URL写进配置文件结果换模型就要改配置上线时候漏改一个环境直接404。更要命的是文心返回的错误码。它不像OpenAI那样有标准化的invalid_request_error而是搞了个error_code加error_msg错误码数字不查文档根本不知道啥意思。比如18是QPS超限110是token无效336501是请求参数错误。你最好在代码里做一个错误码映射表把常见错误翻译成人话再打日志。这里我吃过亏一次线上跑批任务文心返回18我以为是参数问题反复排查请求体结果就是并发太高被限流。后来加了重试和退避才解决。GLM智谱的接入算是三家里最接近OpenAI的但它的“保险丝”藏得深。智谱的API端点https://open.bigmodel.cn/api/paas/v4/chat/completions鉴权用Authorization: Bearer api_key这个没问题。但注意智谱的API Key中间有小数点分隔像xxx.yyy.zzz这种格式。你要是把API Key写在代码里不小心复制漏了中间那段报1000错误还提示“鉴权失败”。排查半天发现是key复制不全很蠢。GLM的另一个特点是“token计费不透明”。它的max_tokens参数你设小了它会截断输出而且不告诉你被截断了。你只看到finish_reason是length不是stop。这时候你要么调大max_tokens要么启用“自动续写”。GLM在v4版本支持一个auto_max_tokens参数设成True它会自动扩长。但我建议不要依赖它因为计费会在你不经意间翻倍。我在生产环境里做硬限制max_tokens1000然后检查finish_reason如果是length就抛出一个“输出超长”的异常由上层做分段处理。这样至少不会花冤枉钱。resprequests.post(URL,json{model:glm-4-flash,messages:messages,max_tokens:1000,temperature:0.3,auto_max_tokens:False# 别开这个钱烧得厉害}).json()ifresp[choices][0][finish_reason]length:# 不要静默截断得让上层知道raiseOutputTooLongError(输出超过1000 tokens请缩小文本或分段)说回通义。通义还有一个别人没有的“优点”——它对超长上下文支持很激进比如qwen-long号称能处理千万级token。但实际调用时如果你的输入特别长通义的首次响应时间会非常久心里要有数。我用它处理过一份100万字的PDF请求发出去等了40多秒才返回第一个token。HTTP客户端超时时间要是设成30秒直接Timeout。所以接通义必须把超时调大建议connect30, read180。别用默认的requests.get(..., timeout5)那是给自己找麻烦。再提一个所有国内大模型都有的问题内容安全审核。你的输入和输出都会过一遍审核系统敏感词会被替换成*或者直接拒绝。这不是bug是合规要求。你拿英文的The quick brown fox jumps over the lazy dog去测没事你拿中文“法院判决书”去测可能触发审核。这里有个实践技巧调用前先自己做个敏感词预检用最简单的词表过滤一遍能省下不少白花花的调用费。因为审核失败一样计费别问我怎么知道的。还有重试策略。面对限流或网络抖动各家返回的错误类型不同你不能一套重试打天下。通义的限流返回HTTP 429但响应体里可能没有Retry-After头。文心返回错误码18重试时要等至少1秒。GLM的限流是HTTP 429但它有个Request-Id头你可以把它打出来方便反馈。我封装了一个带指数退避的重试函数状态码429、500、502、503都会重试但重试三次后依然失败就放弃绝不无限重试。defcall_with_retry(func,*args,retries3,base_delay1.0):foriinrange(retries):try:returnfunc(*args)exceptRateLimitErrorase:waitbase_delay*(2**i)logger.warning(f限流{wait}秒后重试:{e})time.sleep(wait)raiseFinalError(重试了几次还不行放弃)有些细节你一定要注意三家API对temperature的支持范围不一样。通义支持0到2文心只支持0到1GLM支持0到1但官方建议不要超过0.6。你把文心的temperature设成0.7它不报错但会强制截断到1导致结果不稳定。最好在配置里给每家的参数做一个边界限制。关于模型选择我的个人偏好是需要长文本、复杂逻辑用通义qwen-long需要中文理解、少幻觉用文心ernie-4.0需要快速响应、成本敏感用GLMglm-4-flash。这个组合是目前我调参下来性价比最高的。但具体还得看你的场景别盲信多拿自己的测试集跑一遍。接入时候强烈建议做一个适配层把三家API统一成一个接口。不必引入重量级框架自己写个十几行的抽象类就行。我给你们看个我的精简版classLLMClient:defchat(self,messages,**kwargs):raiseNotImplementedErrorclassQwenClient(LLMClient):defchat(self,messages,**kwargs):# 处理qwen的特殊system提示词...classWenxinClient(LLMClient):defchat(self,messages,**kwargs):# 先取token再拼URL...classGLMClient(LLMClient):defchat(self,messages,**kwargs):# 检查max_tokens...这样业务层只依赖LLMClient切换模型只需要换实现类不需要改代码。文章开头那个InvalidApiKey问题就是因为我当初没有这样的适配层直接在业务代码里拼了各家SDK结果排查起来到处找key。最后说说测试。接国内大模型千万不要只在本地环境跑一次就上生产。本地网络通常能直连这些API但生产服务器可能部署在内网出网要走代理代理会缓存HTTP请求。我遇到过服务器上请求通义一直返回旧数据排查半天发现是代理把响应缓存了。所以正式环境里对API请求一定要加Cache-Control: no-cache头并且确认代理不缓存POST请求。写个冒烟测试分别调用三家API的最小模型跑通你好两个字确认网络链路没问题再继续。还有一个差点忘了的坑文本编码。国内API接口默认UTF-8没问题但如果你从数据库读出来的文本是GBK直接塞进JSON里requests库会因为编码不一致报UnicodeDecodeError。统一在入口处做encode(utf-8)处理别偷懒。写到这里我给自己的博客做个小广告式经验总结接国内大模型别把它当OpenAI兼容品它有自己的脾气。通义要糊弄system文心要养tokenGLM要防长输出。每家的错误码、限流策略、超时要求都不同把这些差异封装起来你的代码才能活过一个月。最后记得把所有API Key放进~/.bashrc或者环境变量文件里别硬编码在代码中要不你迟早会在Git提交记录里看到自己的key。下次有机会聊聊流式输出实时打印到Web前端的那点事里面坑更多。先到这我得去喝口水。