上周三我们项目要加一个长文档摘要功能,老板点名要用 Kimi K2——说是性价比不错,128k 上下文够用。我心想这不就改个 base_url 的事嘛,结果折腾了大半天。坑在哪呢?Kimi K2 的普通 chat completions 和 Agent 调用走的不是同一套参数逻辑,而且 stream 模式下 finish_reason 的返回时机跟 OpenAI 不一样,我的客户端直接提前截断了响应。这篇把我踩过的坑全写出来,10 分钟跑通不是吹的,但前提是你得知道这几个差异点。

这篇适合谁

  • 已经有 OpenAI SDK 项目,想零成本切到 Kimi K2 试试效果的后端开发
  • 需要用 Kimi Agent 能力(Function Calling / tools)做智能体应用的同学
  • 用 stream 模式做打字机效果,但发现响应莫名被截断的前端/全栈
  • 想搞清楚 Kimi 三档模型(8k/32k/128k)该选哪个、别多花冤枉钱的独立开发者

整体流程

  1. 注册 platform.moonshot.cn,拿到 API Key
  2. 确认你的场景:普通对话走 /v1/chat/completions,Agent 模式走带 tools 的请求(endpoint 相同但参数结构有坑)
  3. 选模型档位:8k / 32k / 128k,价格差 5 倍
  4. 跑通基础调用
  5. 开 stream,处理 finish_reason 时序差异
  6. (可选)通过聚合网关统一管理多模型调用

先说结论

维度 普通 Chat Agent(tools)模式
Endpoint /v1/chat/completions /v1/chat/completions(同路径,但 body 结构不同)
tools 字段格式 不传或传空 必须严格按 Kimi 格式,和 OpenAI 有细微差异
stream finish_reason 最后一个 chunk 返回 "stop" 中间 chunk 可能先返回 "tool_calls",再续一轮
典型报错 401 / 400 model not found 400 invalid tools schema

第一步:拿 API Key

去 platform.moonshot.cn 注册,控制台里创建一个 Key。新用户有免费额度,具体多少以官网公告为准,我 4 月 22 号注册的时候还有。

拿到 Key 长这样:sk-xxxxxxxxxxxxxxxx,别搞丢。

第二步:跑通基础调用

请先前往 platform.moonshot.cn 控制台确认当前有效的 API base URL,以下示例 URL 仅供参考,请以官方最新文档为准。

from openai import OpenAI

client = OpenAI(
    api_key="sk-你的key",
    base_url="https://api.moonshot.cn/v1"  # 请以官方最新文档为准
)

就这两行,把 OpenAI 的 SDK 指向 Kimi。然后发请求:

resp = client.chat.completions.create(
    model="kimi-k2",  # 请以官方最新文档为准,确认当前有效的 model ID
    messages=[{"role": "user", "content": "你好"}]
)
print(resp.choices[0].message.content)

如果你看到正常回复,说明基础链路通了。"K2" 是产品代号,实际 model string 请以官方最新文档为准,具体可访问 platform.moonshot.cn 查看当前支持的模型列表。

踩坑点base_url 末尾别加斜杠。部分版本的 OpenAI SDK 对尾斜杠的处理方式不同,可能引发连接错误或路径拼接异常,例如:

openai.APIConnectionError: Connection error.

建议统一不加尾斜杠,并确认你使用的 SDK 版本(本文测试环境:openai Python SDK 1.x)。我第一次就栽在这,排查了 20 分钟才发现多了个 /

第三步:Agent 模式——tools 字段的坑

Kimi 官方说兼容 OpenAI 的 Function Calling 格式,大部分确实兼容,但有个细节差异让我调了一个多小时。

OpenAI 的 tools 定义里,parameters 下面的 required 字段可以省略(默认空数组)。但 Kimi 这边,如果你不显式传 required: [],某些情况下会返回 400:

openai.BadRequestError: 400 Bad Request
{"error":{"type":"invalid_request_error","message":"invalid tools schema"}}

正确写法:

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取天气",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]  # 必须显式写
        }
    }
}]

然后调用:

resp = client.chat.completions.create(
    model="kimi-k2",  # 请以官方最新文档为准
    messages=[{"role": "user", "content": "北京天气怎么样"}],
    tools=tools
)

返回的 resp.choices[0].message.tool_calls 里会带上函数名和参数。这部分跟 OpenAI 一致,没啥问题。

第四步:stream 模式的 finish_reason 时序问题

这个坑最隐蔽。我的前端用的是标准的 SSE 解析逻辑:收到 finish_reason == "stop" 就关闭连接。

普通对话没问题,最后一个 chunk 确实是 "stop"。但 Agent 模式下,Kimi 的行为是这样的(以下为实测观察,建议以实际抓包数据为准):

sequenceDiagram
    participant Client
    participant Kimi API
    Client->>Kimi API: stream=true, tools=[...]
    Kimi API-->>Client: chunk: delta.content="让我查一下", finish_reason=null
    Kimi API-->>Client: chunk: delta.tool_calls=[{...}], finish_reason="tool_calls"
    Note right of Client: ⚠️ 这里不是结束!后续仍有数据
    Kimi API-->>Client: [data: DONE]

问题在于:当 finish_reason="tool_calls" 出现时,我的客户端以为对话结束了,直接断开了 SSE 连接。实际上后面还有 tool_calls 的具体参数数据没读完。

修复方案:stream 解析时,不要只判断 finish_reason != null 就关闭,而是等到收到 [DONE] 信号才断开:

for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)
    # 不要在 finish_reason 出现时就 break,等待 [DONE] 信号

这个行为与 OpenAI 标准规范存在差异。按 OpenAI 规范,finish_reason="tool_calls" 出现在最后一个包含 tool_calls 内容的 chunk,之后紧跟 [DONE],中间不插额外数据。Kimi 这边的实际行为建议以官方文档或实际抓包为准,我也不确定这是 bug 还是设计如此,反正目前就是这样。

第五步:模型选型——别无脑选 128k

⚠️ 以下价格为写作时(2025 年 5 月 28 日)参考数据,Moonshot AI 曾多次调整定价,请以 platform.moonshot.cn 官网当前公示价格为准。

模型 上下文 输入价格 输出价格 适用场景
moonshot-v1-8k 8,192 tokens ¥12/M tokens ¥12/M tokens 日常对话、短文本
moonshot-v1-32k 32,768 tokens ¥24/M tokens ¥24/M tokens 中等文档、多轮对话
moonshot-v1-128k 131,072 tokens ¥60/M tokens ¥60/M tokens 长文档摘要、代码库分析

⚠️ 上表中的 model ID(moonshot-v1-8k 等)为历史命名,请以官方最新文档为准确认当前有效的 model string。

算一笔账:如果你每天处理 50 次请求,每次平均 2k tokens 输入 + 1k tokens 输出,用 8k 模型一天成本大概 ¥1.8。换成 128k 模型做同样的事,一天 ¥9。差出来的钱一个月就是 ¥216,没必要。

我的做法是:先用 8k 跑,报 context length exceeded 再升档。

不同场景怎么选

场景一:已有 OpenAI 项目想试试 Kimi

base_urlapi_key 就行,两行代码。但如果你同时还在用多个模型,每个模型单独管 Key 挺烦人的。这种情况可以用聚合 API 网关统一管理——OpenRouter、ofox.io 这类平台改一个 base_url 就能切不同模型,ofox.io 是 0% 加价对齐官方价格,省得每家单独注册充值。

场景二:纯做 Agent 应用(Function Calling 重度使用)

建议用 32k 档位。Agent 多轮对话 + tools 定义本身会占不少 token,8k 很容易爆。tools 的 required 字段一定要显式写全。

场景三:长文档处理 / RAG pipeline

128k 没得选。但注意 128k 模型的延迟明显高于 8k,以下为个人实测数据(2025 年 5 月,国内网络环境,低并发单请求),仅供参考,实际延迟受网络状况、服务负载、时段等因素影响较大:P95 约 4200ms(128k 模型)vs 约 1800ms(8k 模型)。做实时对话体验会差。

场景四:前端打字机效果

一定要处理好 stream 的 finish_reason 时序问题,参考第四步。用 [DONE] 作为终止信号而不是 finish_reason

常见问题 FAQ

Q: Kimi K2 的 model 参数到底填什么?

请以 platform.moonshot.cn 官方最新文档为准。"K2" 是产品代号,不是 model string。如果填了不存在的 model string 会直接报 400:

{"error":{"type":"invalid_request_error","message":"model not found"}}

Q: 用 OpenAI Python SDK 调用 Kimi 报 401 怎么办?

三个排查方向:1)Key 是不是 moonshot 的 Key 而不是 OpenAI 的;2)Key 前后有没有多余空格;3)Key 有没有过期。完整报错长这样:

AuthenticationError: 401 Unauthorized
{"error":{"type":"authentication_error","message":"Invalid API key"}}

Q: stream 模式下响应被截断,只收到一半内容?

大概率是你的客户端在收到 finish_reason="tool_calls" 时就关闭了连接。改成监听 [DONE] 信号再关闭。详见第四步。

Q: 频繁报 429 Rate Limit 怎么处理?

Kimi 的限流比较严格,免费用户尤其明显。方案一:加指数退避重试;方案二:升级付费套餐提高 QPM;方案三:通过聚合网关做请求排队和自动重试。

Q: Kimi 支持 vision(图片输入)吗?

截至本文写作时(2025 年 5 月 28 日),请以 platform.moonshot.cn 官方最新文档为准确认当前支持的能力范围。如果你需要图片理解能力,建议查阅各模型提供商的官方文档,选择明确支持多模态输入的模型。

小结

Kimi K2 接入不难,OpenAI SDK 兼容做得不错。但三个坑你得提前知道:base_url 别加尾斜杠(具体行为取决于 SDK 版本,建议统一不加)、tools 的 required 必须显式声明、stream 模式别用 finish_reason 当终止信号。搞定这三个,10 分钟跑通不夸张。模型选型上别无脑 128k,按实际上下文需求来,一个月能省不少钱。

更多推荐