快速开始
这一页的目标很窄:让你在五分钟内拿到一个真实的模型响应,排除掉环境问题。 效果评估和成本测算留到后面几页。
1 · 获取 API Key
在 koach 控制台 的「API Keys」页面创建密钥。 密钥分两种,前缀不同:
| 前缀 | 环境 | 额度 | 说明 |
|---|---|---|---|
sk-test- | 沙箱 | 500 万 token / 账号 | 不计费。请求与响应 30 天后清理,不用于任何模型训练 |
sk-live- | 生产 | 按合同 | 计费。需要完成企业实名与合同签署后开通 |
它等同于你账户的支付凭证,任何拿到密钥的人都可以消耗你的额度。 不要写进前端代码、不要提交进 Git 仓库、不要放进移动端安装包 —— 移动端应当由你自己的服务端代理转发,密钥留在服务端。
怀疑泄露时在控制台直接吊销,吊销即时生效。
把密钥放进环境变量,后面所有示例都从这里读:
export KOACH_API_KEY="sk-test-••••••••••••••••"
export KOACH_BASE_URL="https://api-sandbox.koach.ai/v1"
2 · 第一次调用
先用一个最小请求确认链路通畅。这个问题故意选了一个通用模型容易答错的方向 —— 跑量增长的安全上限。
curl "$KOACH_BASE_URL/chat/completions" \
-H "Authorization: Bearer $KOACH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "keepace-pro",
"messages": [
{"role": "user", "content": "我现在周跑量 30 公里,想两个月后跑半马,怎么加量比较安全?"}
]
}'
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["KOACH_API_KEY"],
base_url=os.environ["KOACH_BASE_URL"],
)
resp = client.chat.completions.create(
model="keepace-pro",
messages=[{
"role": "user",
"content": "我现在周跑量 30 公里,想两个月后跑半马,怎么加量比较安全?",
}],
)
print(resp.choices[0].message.content)
print("消耗 token:", resp.usage.total_tokens)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.KOACH_API_KEY,
baseURL: process.env.KOACH_BASE_URL,
});
const resp = await client.chat.completions.create({
model: "keepace-pro",
messages: [{
role: "user",
content: "我现在周跑量 30 公里,想两个月后跑半马,怎么加量比较安全?",
}],
});
console.log(resp.choices[0].message.content);
console.log("消耗 token:", resp.usage.total_tokens);
响应结构
与 OpenAI 一致,多了一个 koach 字段用于回传领域相关的元信息。
{
"id": "chatcmpl-8Kq2nR4vXpL",
"object": "chat.completion",
"created": 1771305600,
"model": "keepace-pro-20260601",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "两个月从周跑量 30 公里备战半马是可行的,关键是加量节奏……"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 38,
"completion_tokens": 512,
"total_tokens": 550
},
"koach": {
"domain": "training.endurance",
"safety_level": "general",
"knowledge_refs": ["acsm.progression.10pct", "keep.kb.halfmarathon.plan"]
}
}
general 表示常规建议;caution 表示已触发损伤或疾病相关的谨慎策略;
referral 表示模型判断应当建议用户就医,此时回答里会包含转诊提示koach_cite: false 关闭3 · 开启流式输出
运动健康的回答通常偏长(一份训练计划几百字很常见),非流式的等待感很明显。
加上 stream: true 走 SSE:
stream = client.chat.completions.create(
model="keepace-pro",
messages=[{"role": "user", "content": "给我一份四周的半马进阶计划"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
const stream = await client.chat.completions.create({
model: "keepace-pro",
messages: [{ role: "user", content: "给我一份四周的半马进阶计划" }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
curl -N "$KOACH_BASE_URL/chat/completions" \
-H "Authorization: Bearer $KOACH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "keepace-pro",
"messages": [{"role": "user", "content": "给我一份四周的半马进阶计划"}],
"stream": true
}'
SSE 每一帧是一个 data: 行,最后以 data: [DONE] 结束:
data: {"id":"chatcmpl-8Kq2nR4vXpL","choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"id":"chatcmpl-8Kq2nR4vXpL","choices":[{"delta":{"content":"第一周"},"index":0}]}
data: {"id":"chatcmpl-8Kq2nR4vXpL","choices":[{"delta":{"content":"以适应为主"},"index":0}]}
data: {"id":"chatcmpl-8Kq2nR4vXpL","choices":[{"delta":{},"index":0,"finish_reason":"stop"}],
"usage":{"prompt_tokens":21,"completion_tokens":486,"total_tokens":507}}
data: [DONE]
如果你要统计成本,别在中间帧里找 usage,它只出现在带
finish_reason 的那一帧上。
4 · 带上用户档案
上面的回答是通用的,因为模型对这个用户一无所知。传入 koach_profile 之后,
模型会结合该用户的训练历史、身体状况与目标来回答 —— 这是 koach 与通用模型最主要的差别。
档案的写入方式见记忆大脑。
resp = client.chat.completions.create(
model="keepace-pro",
messages=[{"role": "user", "content": "这周该怎么练?"}],
extra_body={
"koach_profile": "usr_8f2a41", # 你的用户在记忆大脑里的 ID
"koach_safety": "strict", # 触及医疗边界时更保守
},
)
同一个问题「这周该怎么练?」,不带档案时模型只能反问;带上档案后它知道这个用户 上周跑了 42 公里、有过跟腱不适、目标是十月的全马,于是会直接给出减量周的建议。
5 · 处理错误
非 2xx 响应统一返回下面这个结构。生产接入前请至少处理
429(限流)和 5xx(服务端),这两类是可重试的。
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "每分钟请求数超出上限,请在 1.4 秒后重试。",
"param": null,
"request_id": "req_01JQ7X2M9K"
}
}
每个响应的 header 和错误体里都有 request_id。
有它我们能直接定位到那一次调用的完整链路,没有的话排查会慢很多。
完整的状态码、错误类型与建议的退避策略见 错误码与重试。