用 Python 接入火山方舟:从 AK/SK 到两套 Plan 密钥
我用同一个火山引擎账号,把 Coding Plan 和 Agent Plan 分别接到了 Python。两套配置各调用一次 Chat Completions、一次 Responses,四次请求都返回了 PLAN_OK。
这篇文章保留完成这件事所需的步骤:在哪里拿 Key、AK/SK 如何参与密钥管理、两个地址怎样区分,以及代码究竟返回了什么。示例里的凭据均为占位符;完整密钥不进入文章、仓库或下载包。
先把两个地址配对
Coding Plan 与 Agent Plan 可以使用相同的 OpenAI Python SDK,但不能因此共用一份含糊的连接配置。我为它们建立了两个 ChatEnv profile:
| 配置 | Base URL | 本次实测模型 |
|---|---|---|
volcengine-coding-plan | https://ark.cn-beijing.volces.com/api/coding/v3 | doubao-seed-2.0-code |
volcengine-agent-plan | https://ark.cn-beijing.volces.com/api/plan/v3 | doubao-seed-2.0-code |
Base URL 指到版本前缀即可,SDK 会继续拼接 /chat/completions 或 /responses。不要再手工把这两个后缀填到 base_url 中。
普通方舟接口的 /api/v3 不在这份配置里。即使普通接口也能接受某个模型名,也不能据此认为请求消耗的是 Plan 套餐。本文脚本会检查完整 Base URL,不匹配就退出,也不配置按量入口作为失败后的备选。
还有一项独立检查:Agent Plan 的“超额后付费”。地址正确并不代替费用设置检查。我没有开启这个开关;下面的调用也只发送短提示词,关闭自动重试,并限制输出长度。它们是连通性测试,不是对额度耗尽行为的压力测试。
上述模型名在本次请求中实际可用,但官方页面的模型清单仍在更新。新账号应优先以自己套餐页面展示的 Model ID 为准;不要把这里记录的实测模型名当作长期支持承诺。
AK/SK 和 API Key 做不同的事
如果目的是让应用问模型一个问题,应用只需要 API Key。把整个云账号的 AK/SK 交给应用,没有必要。
AK/SK 用在控制面:给管理请求签名,查询身份、套餐和密钥,再按权限创建密钥。API Key 用在模型调用面:通过 Bearer 鉴权发起生成请求。
这条流程不会创建新的火山云账号,也不会设置网页登录密码。后文说的“创建”,具体对象都是 API Key。购买套餐、创建 IAM 用户、重置密码、更新后付费设置,是另外的操作,不应悄悄塞进一个接入脚本。
获取密钥:先查现有记录,再决定是否创建
在控制台操作
已经购买套餐的读者,可从 Coding Plan 快速开始进入普通方舟 API Key 管理页;Agent Plan 则从 Agent Plan 快速开始进入个人套餐的专属 Key 区域。复制时取完整值,不要复制列表里的掩码。两套 Key 分别保存,随后按本文的两个 Base URL 调用。
如果页面没有专属 Key,先确认当前套餐、账号和项目,再创建。不要为了“得到一把新 Key”点击更新或重置旧 Key:旧应用可能还在用它。
用 AK/SK 自动完成
本次最容易混淆的是两个同名的 ListApiKeys:IAM 的密钥列表,不等于 Ark 服务的 API Key 列表。调用时不仅要看 Action,还要核对服务、域名和 API 版本。
这次实际使用的控制面是:
服务:ark
地域:cn-beijing
域名:ark.cn-beijing.volcengineapi.com
版本:2024-01-01
方法:POST
Agent Plan 个人版的查询请求还要带场景过滤:
{
"ProjectName": "default",
"PageSize": 100,
"Filter": {
"Scene": "RealAgentPlanPersonal"
}
}
这是本次从控制台公开前端调用路径核对、再用真实账号验证的契约。它不等于承诺所有内部管理 Action 都有长期稳定的公开接口;自动化应保留错误即停的处理,不应在失败时猜另一个 Action 名继续试。
我查到这个场景的密钥列表为空,才执行一次 CreateApiKey。请求体如下:
{
"Name": "chatenv-agent-plan",
"ProjectName": "default",
"ResourceInstances": [
{"ResourceId": "*", "ResourceType": "all"}
],
"Scene": "RealAgentPlanPersonal"
}
成功响应提供密钥 ID。随后必须重新调用带相同场景过滤的 ListApiKeys,核对名称、ID 和 Active 状态,再用 GetRawApiKey 读取完整值:
body = {"Id": selected_key["Id"]}
Id 保留列表返回的原始类型;不要把数字 ID 擅自转成字符串。
这里展示的资源参数来自本次控制台创建路径,并不是一份建议照搬到所有 IAM 凭据上的最小权限策略。生产自动化的 AK 应限制到需要的管理操作;普通模型调用程序不应持有 AK/SK。
Coding Plan 这次复用了已经存在、随后通过套餐入口验证成功的 Key,没有新建或轮换。尤其不要拿普通 Ark 列表里的第一条记录,未经核对就自动标记成 Coding Plan Key。存在多条记录时,操作人要明确选择 ID,程序再验证状态和实际调用。
准备 Python 环境
本次使用 Python 3.12,依赖版本为 chatenv 0.2.11、openai 2.54.0、requests 2.34.2。已有合适环境可以复用;新机器可以用 uv 建一个独立环境。以下为 macOS/Linux shell 命令:
uv venv --python 3.12 "$HOME/.chatarch/volcengine/.venv"
source "$HOME/.chatarch/volcengine/.venv/bin/activate"
uv pip install "chatenv==0.2.11" "openai==2.54.0" "requests==2.34.2"
把下方程序保存到自己的工作目录。运行模型程序不需要 AK/SK;只有管理密钥的自动化脚本需要它。
把两套配置放进 ChatEnv
我把密钥放在命名 profile 中,没有切换全局默认配置。这样,已有应用不会因为一次验证换到另一个账号或另一个计费入口。
下面的程序通过隐藏输入读取 Key,并使用 ChatEnv 自己的存储接口写入。它不把密钥放进命令行参数,也不会覆写已有的不同密钥。
import argparse
from getpass import getpass
from chatenv import EnvStore, OpenAIConfig, get_paths
BASES = {
"volcengine-coding-plan":
"https://ark.cn-beijing.volces.com/api/coding/v3",
"volcengine-agent-plan":
"https://ark.cn-beijing.volces.com/api/plan/v3",
}
parser = argparse.ArgumentParser()
parser.add_argument("profile", choices=BASES)
args = parser.parse_args()
key = getpass("粘贴完整 API Key(输入不回显): ").strip()
if not key or "*" in key:
raise SystemExit("需要完整密钥,不能使用掩码。")
store = EnvStore(get_paths().envs_dir)
old = store.load_profile(OpenAIConfig, args.profile)
if old.get("OPENAI_API_KEY") not in (None, "", key):
raise SystemExit("该 profile 已有不同密钥;停止,未覆盖。")
values = {
"OPENAI_API_KEY": key,
"OPENAI_API_BASE": BASES[args.profile],
"OPENAI_API_MODEL": "doubao-seed-2.0-code",
}
path = store.save_profile(OpenAIConfig, args.profile, values)
assert store.load_profile(OpenAIConfig, args.profile) == values
print(f"已保存 {args.profile},未切换默认配置。")
print(f"文件:{path}")
运行两次,每次输入对应套餐页面取得的密钥:
python save_plan_profile.py volcengine-coding-plan
python save_plan_profile.py volcengine-agent-plan
实际生成的配置结构是:
OPENAI_API_KEY=替换为你的完整CodingPlanKey
OPENAI_API_BASE=https://ark.cn-beijing.volces.com/api/coding/v3
OPENAI_API_MODEL=doubao-seed-2.0-code
OPENAI_API_KEY=替换为你的完整AgentPlanKey
OPENAI_API_BASE=https://ark.cn-beijing.volces.com/api/plan/v3
OPENAI_API_MODEL=doubao-seed-2.0-code
默认位置是 ~/.chatarch/envs/OpenAI/。本次写入后,两份文件均验证为 0600。程序从指定 profile 读取,不需要先 source 文件,也不会从进程里碰巧存在的 OPENAI_API_KEY 偷换凭据。
完整调用代码
下面就是实测用的程序。它没有隐藏的辅助服务,不需要启动代理服务器或数据库。
#!/usr/bin/env python3
"""Run one bounded request through a named ChatEnv Plan profile."""
import argparse
import json
from urllib.parse import urlsplit
from chatenv import EnvStore, OpenAIConfig, get_paths
from openai import OpenAI
BASES = {
"volcengine-coding-plan":
"https://ark.cn-beijing.volces.com/api/coding/v3",
"volcengine-agent-plan":
"https://ark.cn-beijing.volces.com/api/plan/v3",
}
def run(profile: str, protocol: str) -> dict:
values = EnvStore(get_paths().envs_dir).load_profile(
OpenAIConfig, profile
)
base = values.get("OPENAI_API_BASE", "").rstrip("/")
if base != BASES[profile]:
raise ValueError("Profile must use its exact Plan endpoint; no fallback.")
key = values.get("OPENAI_API_KEY", "")
if not key or "*" in key:
raise ValueError("Missing real API key; masked values are not credentials.")
model = values["OPENAI_API_MODEL"]
with OpenAI(api_key=key, base_url=base, timeout=30, max_retries=0) as client:
if protocol == "responses":
result = client.responses.create(
model=model,
input="只回复 PLAN_OK,不要解释。",
max_output_tokens=32,
extra_body={"thinking": {"type": "disabled"}},
)
text = result.output_text
else:
result = client.chat.completions.create(
model=model,
messages=[{
"role": "user",
"content": "只回复 PLAN_OK,不要解释。",
}],
max_tokens=32,
extra_body={"thinking": {"type": "disabled"}},
)
text = result.choices[0].message.content
return {
"profile": profile,
"protocol": protocol,
"path": urlsplit(base).path + (
"/responses" if protocol == "responses"
else "/chat/completions"
),
"model": model,
"text": text,
"usage": result.usage.model_dump() if result.usage else None,
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", choices=BASES, required=True)
parser.add_argument(
"--protocol", choices=["responses", "chat"], default="responses"
)
args = parser.parse_args()
print(json.dumps(
run(args.profile, args.protocol), ensure_ascii=False, indent=2
))
Responses 的结果从 output_text 读取;Chat Completions 则从 choices[0].message.content 读取。两者都兼容 OpenAI SDK,不意味着请求体和响应结构可以混用。
thinking 是本次所用模型接受的扩展字段,通过 extra_body 传入。换模型时要重新核对它是否支持,不能把这行当成所有供应商通用的参数。这里关闭思考,只为让一次短连通性测试迅速结束。
运行命令:
python plan_demo.py --profile volcengine-coding-plan --protocol responses
python plan_demo.py --profile volcengine-coding-plan --protocol chat
python plan_demo.py --profile volcengine-agent-plan --protocol responses
python plan_demo.py --profile volcengine-agent-plan --protocol chat
四次请求实际返回了什么
下面是 Agent Plan 的 Responses 真实输出;只保留程序打印的字段,没有展示凭据或账号标识:
{
"profile": "volcengine-agent-plan",
"protocol": "responses",
"path": "/api/plan/v3/responses",
"model": "doubao-seed-2.0-code",
"text": "PLAN_OK",
"usage": {
"input_tokens": 53,
"input_tokens_details": {
"cache_write_tokens": null,
"cached_tokens": 0
},
"output_tokens": 3,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 56
}
}
四次调用的汇总如下。Chat Completions 原始字段名是 prompt_tokens / completion_tokens,表中统一列为输入与输出,方便比较。
| 套餐 | 协议 | 返回文本 | 输入 token | 输出 token |
|---|---|---|---|---|
| Coding Plan | Responses | PLAN_OK | 53 | 3 |
| Coding Plan | Chat Completions | PLAN_OK | 53 | 3 |
| Agent Plan | Responses | PLAN_OK | 53 | 3 |
| Agent Plan | Chat Completions | PLAN_OK | 53 | 3 |
这些结果证明的是:在本次账号、模型与两套 Plan 入口下,两种文本协议都完成了鉴权和生成。它们不证明全部模型可用、不证明工具调用或多模态协议兼容,也不是速率上限测试。返回的 token 统计不是账单,不能拿 total_tokens 直接推导扣了多少套餐额度或人民币。
Seed Evolving 和语音识别怎样接入
在前面的协议验证之后,我又分别用两把 Key 调用 doubao-seed-evolving,两次都返回 EVOLVING_OK,各统计输入 53、输出 5 token。要改用它,只需把对应 profile 的 OPENAI_API_MODEL 设置为 doubao-seed-evolving;不要连同 Key 和地址一起混换。
两套模型列表的完整快照也放在工具包 models-2026-09-08.json。2026-09-08 管理 API 返回 Coding 15 个、Agent 21 个标识(含路由别名,不是底层模型数量)。两者共有:
ark-code-latest、deepseek-latest、deepseek-v4-flash、deepseek-v4-pro、doubao-seed-2-1-turbo、doubao-seed-2.0-lite、doubao-seed-evolving、glm-5.2、glm-5.3、glm-5.3-flash、glm-latest、kimi-k2.7-code、kimi-latest、minimax-latest、minimax-m3。
Agent 清单另有:doubao-embedding-vision、doubao-seed-2.0-mini、doubao-seedance-2.0、doubao-seedance-2.0-fast、doubao-seedream-5.0-lite、kimi-k3。其中图像、视频和 Embedding 要走各自协议,不是普通聊天候选。语音模型另由语音接入文档列出,因此不能只靠这份文本/多模态模型列表判断 ASR 是否可用。
这两套套餐当前公开模型清单都包含 Seed Evolving。Agent Plan 还提供语音识别,但它是另一条接口:doubao-seed-asr-2.0,不是把音频塞给 Seed Evolving 的 Chat Completions。
按照官方语音接入文档,ASR 使用 Agent Plan 专属 Key,走 WebSocket,资源标识为 volc.seedasr.sauc.duration:
- 双流:
wss://openspeech.bytedance.com/api/v3/plan/sauc/bigmodel_async,边发送音频边接收识别结果。 - 单流:
wss://openspeech.bytedance.com/api/v3/plan/sauc/bigmodel_nostream,偏准确率优先,结果返回时机按接口协议处理。
调用头用 X-Api-Key 和 X-Api-Resource-Id,不是前面 SDK 的 Bearer 请求。官方还提供 doubao-seed-tts-2.0 做文本转语音。这些语音能力在本文只核对了接入文档,没有上传音频实测;语音抵扣、权限和后付费设置也需要单独确认,不能由文本请求成功推断出来。
复用 AK/SK 管理脚本
下载完整 Python 工具包。包内包含签名和管理脚本、命名配置保存、模型调用示例、依赖清单、测试及完整说明;不含任何真实凭据。
先按包内 README 准备 Python 环境,再把自己授权的 AK/SK 放入仅本人可读的凭据文件,或通过无回显输入送进 stdin。无需把 AK/SK 写入文章里的生成程序。
下面是管理脚本的最小使用顺序。PY 指向已安装依赖的 Python;资源 ID 来自你自己的密钥列表,不是 API Key 明文。
export PY="$HOME/.chatarch/volcengine/.venv/bin/python"
"$PY" scripts/plan_toolkit.py identity
"$PY" scripts/plan_toolkit.py plan --kind coding
"$PY" scripts/plan_toolkit.py plan --kind agent
"$PY" scripts/plan_toolkit.py list-keys --kind coding --show-ids
"$PY" scripts/plan_toolkit.py list-keys --kind agent --show-ids
确认列表中的目标后,通过 ensure-existing 取回并保存。Coding 的普通密钥库存不代表其中每把都可用于 Coding Plan,因此必须自己确认并指定 ID,脚本不会擅自挑第一把。
"$PY" scripts/plan_toolkit.py ensure-existing \
--kind coding --key-id "$CODING_KEY_ID" --profile volcengine-coding-plan
"$PY" scripts/plan_toolkit.py ensure-existing \
--kind agent --key-id "$AGENT_KEY_ID" --profile volcengine-agent-plan
若将来为另一个已开通套餐的账号接入,并且 Agent 专属密钥确实尚未生成,才使用下面的创建入口:
"$PY" scripts/plan_toolkit.py create-agent \
--profile volcengine-agent-plan --create-missing-agent-key
它在发送前重新查询库存,并以排他文件记录创建意图;请求超时之后只读回列表,不重发创建。遇到“结果未知”时按 README 核对控制台,不要删除标记后循环重试。已有配置不一致时拒绝覆盖,也不会激活全局默认 profile。这里没有云账号、IAM 用户或登录密码的创建功能。
原始控制脚本已真实完成一次 Agent Key 创建;整理后的工具包重新验证了身份、套餐、密钥查询以及已有 profile 不变。新工具包的创建恢复分支只做离线测试,没有为了验收再制造一把 Key。
失败时,停在哪一步
第一次执行程序时,本机没有安装 openai,直接报 ModuleNotFoundError。这是本地依赖失败,尚未发出模型请求。安装 SDK 后再执行,才得到上面的真实结果。不要把程序退出和“模型服务拒绝请求”混为一谈。
接入后常见问题可以沿着请求路径排查:
- 凭据错误或 401:确认复制的是完整 Key,profile 名称没选错;不要先去生成第三把 Key。
- 403:检查 AK 的管理权限或 API Key 对应的服务权限。提高重试次数不能修复授权。
- 模型不可用:在当前套餐页面核对模型标识。不要把自定义推理接入点 ID 与模型 ID 混用。
- 429 或额度不足:停止自动重试,检查套餐用量与刷新窗口。不要自动退回普通
/api/v3。 - 创建密钥超时:先重新查列表,按本次名称和 ID 核对是否已经创建。超时只说明客户端没拿到确定结果,不能据此再建一把。
- 保存时发现不同密钥:停止覆盖,确认到底是同一账号的旧配置,还是选错 profile。默认配置不应在恢复流程里被顺手切换。
对于没有文档支持的管理 Action,错误即停也很重要。本次曾核对一个看似合理的“获取个人套餐密钥”Action 名,服务端返回 404;最后采用的是控制台实际使用的 ListApiKeys → GetRawApiKey 路径。API 名称不能靠英文拼词推断出来。
把验证变成可重复的操作
下一次在新机器上接入,不需要重新追一遍控制台代码。操作顺序应固定下来:先验证 AK 身份与套餐,再选已有 Key;只有 Agent Plan 场景列表确认为空、操作者明确要求创建时才创建;写入命名 ChatEnv profile 后,再手动发起短调用。
把创建与测试拆开有一个直接好处:只想检查配置的人,不会意外创建密钥或消耗模型额度。真实模型请求也不应混进单元测试里反复运行。
本文配套脚本包将这些边界放进命令接口:管理动作白名单、明确的创建开关、已存在密钥的复用、profile 覆写保护,以及独立的短调用命令。公共下载包不包含本次账号凭据。
资料与验证边界
本文记录的是 2026-09-08 的一次真实接入。套餐价格、支持模型与额度规则会变化;购买和正式上线前,仍需打开对应套餐页面核对。本文不把当日可用的模型列表写成永久支持承诺。
- Coding Plan:套餐概览、快速开始。
- Agent Plan:套餐概览、快速开始。
- API Key 管理与 Base URL 及鉴权。
- 超额后付费规则、管理开关、AFP 抵扣规则。
- Coding Plan 模型列表、Agent Plan 模型列表。
- 查询个人版套餐、获取 AFP 额度、查询模型限流。
接口取证还包括方舟控制台公开前端模块:Agent Plan 页面使用的 Scene、ListApiKeys、CreateApiKey 和 GetRawApiKey 已与真实请求逐项核对。可从 Agent Plan 控制台进入;控制台内部契约与有版本的公开文档应分别对待。