我给智课工坊加了个双通道 API 代理:一次 Token 焦虑引起的架构改造
这不是教程,是一次架构决策的记录。智课工坊做课一直稳定,直到换了一个 API 网关,课件生成开始频繁失败。排查到最后发现是推理模型在"偷吃"输出 tokens。解决思路是双通道代理——这个思路其实通用,任何 AI 应用只要同时有"纯生成"和"需推理"两种调用场景,都可以复用。
起点:一切都好,直到换了网关
智课工坊的后端很简单——前端单文件 HTML 通过 nginx 反向代理,调 /api/chat 到后端 Python 服务,后端再转发给大模型。最早的部署用的是 DeepSeek 官方 API,模型是 deepseek-chat,跑了大半个月一直稳定。老师上传 PDF 教材、AI 自动出课件、逐页生成幻灯片、配题库、导出 ZIP 课程包——整套六步流水线从头跑到尾,从来没掉过链子。
问题出在换了一个 API 网关之后。新网关的 base URL 和 key 替换上去,前端一点生成课件——第一步就报错:“第 1 课生成异常,自动重试中(已放宽页数要求)”。重试三次,全部失败,最后兜底生成一个基础版课件。不是偶发的网络抖动,是稳定复现的失败。
一开始怀疑网关有频率限制或者请求被截断,就在服务器上直接 curl 了一把:
# 直接调用网关 API,简单回复
curl -s -X POST https://<gateway>/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <key>" \
-d '{"model":"deepseek-v4-flash-0731","messages":[{"role":"user","content":"Say hi"}],"max_tokens":200}'
返回 200,内容正常。网关没问题。
第一次推翻:不是网关的问题,是推理模型
再仔细看返回的 JSON,发现一个异常字段:
{
"choices": [{
"message": {
"content": "Hi",
"reasoning_content": "The user said 'Say hi'..."
}
}],
"usage": {
"completion_tokens_details": {
"reasoning_tokens": 22
}
}
}
reasoning_tokens: 22。这个网关把所有模型都加了一层推理。deepseek-v4-flash 按理是不带推理的快速模型,但经过这个网关后,每次调用都会先"思考"再输出。
对于一句 “Say hi”,消耗 22 个 reasoning tokens 无所谓。但智课工坊生成课件时,前端发的 prompt 长这样(简化版):
你是课件作者,遵循淡蓝科技风课件规范:每课 = 封面 + 内容页 + 实操步骤 +
图示页 + 课堂互动 + Prompt模板 + 总结页。课件语言口语化、实操导向。
本课详细内容:
- 学习目标:3 条
- 知识点:5 个
- 实操步骤:4 个
- Prompt 模板:2 个
...
输出 JSON,slides 数组,每页带 type/h2/paras/art/design 字段。
至少 10 页,最多 16 页。
这个 system prompt 大约 4000+ tokens,加上 user prompt 里的课程详细内容,输入接近 6000 tokens。模型拿到这么长的 prompt 之后,"思考"过程显著膨胀——实测一次课件生成,reasoning_tokens 能吃掉 2000-4000 tokens。前端设置的 maxTokens=8000,减去推理消耗,留给正文输出的只有 4000-6000 tokens。而一个 10 页课件的完整 JSON 文本,轻松超过 6000 tokens。
于是模型在输出到第 7、8 页时被截断,返回一个不完整的 JSON。前端 extractJSON() 解析失败 → 报"生成异常" → 重试 → 同样截断 → 三次失败 → 兜底。
结论:不是网关挂了,是推理模型不适合做纯文本生成任务。
第二次推翻:换 flash 模型也没用
网关的模型列表里有 deepseek-v4-flash-0731,看名字是不带推理的。切过去试——它的 reasoning_tokens 确实小了很多(46 tokens vs 之前几百),但仍然有推理层。因为网关统一加了一个推理 wrapper,不管什么模型进来都先走一遍 thinking。
又试了 glm-5 和 glm-5.1——更惨,50 个 maxTokens 全部被推理吃光,content 字段直接为空。
网关上的 7 个模型,没有一个能彻底去掉推理。这堵死了"换模型"这条路。
方案:双通道 API 代理
既然网关无法去掉推理,那就把推理任务和生成任务分流:
智课工坊前端
├─ chatLLM() → /api/chat → 网关 (deepseek-v4-pro) 推理/分析
└─ genLLM() → /api/gen → 官方 (deepseek-chat) 纯生成
/api/chat继续走网关的推理模型,处理课程方案设计、内容审校等需要"先分析再输出"的任务/api/gen走 DeepSeek 官方 API 的deepseek-chat——这个模型没有 reasoning_content,100% 的 tokens 全部用于正文输出
同一个 Python 服务,两个 handler,共享一套鉴权和 CORS。实现极其简单:
# proxy_chat: 推理通道 → 网关
def proxy_chat(handler, body):
url = GATEWAY_BASE + '/chat/completions'
status, j = http_post_json(url, headers, {
'model': 'deepseek-v4-pro',
'messages': body['messages'],
'max_tokens': min(body.get('maxTokens') or 3000, 8000)
})
send_json(handler, status, j)
# proxy_gen: 生成通道 → 官方 DeepSeek
def proxy_gen(handler, body):
url = 'https://api.deepseek.com/chat/completions'
status, j = http_post_json(url, headers, {
'model': 'deepseek-chat', # 无推理,100% 输出
'messages': body['messages'],
'max_tokens': min(body.get('maxTokens') or 3000, 16000) # 生成任务给更大空间
})
send_json(handler, status, j)
前端只改了一行:课件生成、题库生成、讲稿生成三个入口从 chatLLM() 改成 genLLM(),后者调 /api/gen 而非 /api/chat。其他所有逻辑不变。
切换后第一次测试,课件生成返回 517 字符的完整 JSON,has_reasoning_content: False,一次通过。
这个设计的可复用性
双通道代理不是智课工坊专属的方案。任何 AI 应用只要同时存在两类调用场景,都可以用同样的架构:
| 场景 | 模型选择 | 通道 |
|---|---|---|
| 长文本生成(文章、课件、报告) | 无推理模型(deepseek-chat) | 生成通道 |
| 分析/规划/审校 | 推理模型(deepseek-v4-pro) | 推理通道 |
| 简单问答/对话 | 任意 | 按延迟和成本选 |
关键原则就一条:不要把 tokens 花在用户看不见的地方。 生成任务的每一个 token 都应该在 content 字段里、最终展示给用户。推理固然有价值,但它的价值在于"帮用户理清思路",不是在"生成一个 JSON"的时候先跟自己聊几千字再动手。
总结
这次改造的教训:
- API 网关不是透明的。 它可能在你不注意的地方加推理层、改模型行为。接入新网关时,先看返回的
usage字段里有没有reasoning_tokens。 - 推理模型和生成模型是两种东西。 前者适合分析、规划、审校,后者适合大批量文本输出。混用的代价是 output tokens 被偷吃,前端看到的是截断的 JSON。
- 双通道代理是一个轻量方案。 不需要微服务、不需要消息队列,同一个 Python 进程加一个 handler,前端多一个函数,就能把两类任务分开路由。成本极低,收益明显。
华为云开发者发展与支持部 · 科技博主系列
- 点赞
- 收藏
- 关注作者
评论(0)