我给智课工坊加了个双通道 API 代理:一次 Token 焦虑引起的架构改造

举报
阿诺林 发表于 2026/08/30 00:09:27 2026/08/30
【摘要】 这不是教程,是一次架构决策的记录。智课工坊做课一直稳定,直到换了一个 API 网关,课件生成开始频繁失败。排查到最后发现是推理模型在"偷吃"输出 tokens。解决思路是双通道代理——这个思路其实通用,任何 AI 应用只要同时有"纯生成"和"需推理"两种调用场景,都可以复用。 起点:一切都好,直到换了网关智课工坊的后端很简单——前端单文件 HTML 通过 nginx 反向代理,调 /api/...

这不是教程,是一次架构决策的记录。智课工坊做课一直稳定,直到换了一个 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-5glm-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"的时候先跟自己聊几千字再动手。

总结

这次改造的教训:

  1. API 网关不是透明的。 它可能在你不注意的地方加推理层、改模型行为。接入新网关时,先看返回的 usage 字段里有没有 reasoning_tokens
  2. 推理模型和生成模型是两种东西。 前者适合分析、规划、审校,后者适合大批量文本输出。混用的代价是 output tokens 被偷吃,前端看到的是截断的 JSON。
  3. 双通道代理是一个轻量方案。 不需要微服务、不需要消息队列,同一个 Python 进程加一个 handler,前端多一个函数,就能把两类任务分开路由。成本极低,收益明显。

华为云开发者发展与支持部 · 科技博主系列

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。