1. 从「人盯会话」到「工单驱动」:Symphony 想解决的真实痛点
如果你最近半年在折腾 Codex 这类编码智能体,大概率经历过同一个场景:同时开三四个会话,一个在改前端组件,一个在跑测试,还有一个卡在依赖安装上等你确认。你像个救火队长一样在标签页之间来回切,切到最后自己都忘了哪个会话在干什么。这不是你效率低,而是交互式智能体本身的天花板——它把「调度」这件事留给了人。
Symphony 这个开源规范想做的事情,就是把这层调度从人手里拿走。它的核心主张只有一句话:每一个未关闭的任务,都应该有一个专属的智能体在工作空间里持续运行,人只负责审查结果。换句话说,它把 Linear 这类项目管理面板变成了编码智能体的控制平面,工单状态就是状态机,智能体从看板上拉活干。
这套东西适合谁?我梳理了三类:
第一类是小团队里已经在用 Codex 做日常开发、但被上下文切换拖垮的人。你不需要重写整个工作流,只需要把「分配任务」这个动作从聊天窗口挪到工单系统。
第二类是做基础设施迁移、依赖升级这种「任务树」型工作的团队。Symphony 的阻塞依赖机制在这种场景下特别顺手,比如「React 升级」被「迁移到 Vite」阻塞,智能体会自动等前置任务完成再动手。
第三类是想研究智能体编排架构的开发者。Symphony 本身在技术上只是一个 SPEC.md 文件加一份参考实现,它的价值在于展示了一种「最小化编排层」的设计思路,而不是一个开箱即用的产品。
需要提前说清楚的是,Symphony 不是一个你 clone 下来就能跑的成品。它是一份规范,官方参考实现用 Elixir 写的,但社区已经用 TypeScript、Go、Rust、Java、Python 各自实现过一遍,每种语言都跑通了。这意味着你完全可以用自己团队最熟的语言去落地它。下面我会从项目结构、配置约定到运行验证,给出一套可复用的接入流程,重点放在「怎么在本地跑通并确认编排行为符合预期」上。
2. 接入前的准备:TaoToken 作为模型调用入口的配置
在动手写 Symphony 的配置之前,得先把模型调用这条链路打通。Symphony 本身只负责编排逻辑,它需要调用 Codex 来实际执行编码任务,而 Codex 的请求最终要落到一个可用的 API 端点上。我实测下来,用 TaoToken 作为统一入口比较省事,因为它同时兼容 OpenAI 风格的接口和 Anthropic 风格的接口,Symphony 里切换模型时不用改两套配置。
先说清楚 TaoToken 是什么:它是一个大模型 API 聚合服务,提供 OpenAI 兼容的/v1/chat/completions和/v1/responses接口,也提供 Anthropic 兼容的/v1/messages接口。对于 Symphony 这种需要频繁调用 Codex 的场景,它的价值在于你不用为每个模型单独维护一套鉴权和计费逻辑。
第一步是拿 Key。访问 https://taotoken.net/api-keys ,登录后创建一个新的 API Key。建议按项目维度创建,比如给 Symphony 单独建一个,方便后面排查问题时区分调用来源。创建完记得立刻复制,页面刷新后就看不到了。
第二步是确认 Base URL。TaoToken 的 API 根地址是:
https://taotoken.net/api注意这个地址不带任何查询参数,直接作为 Base URL 使用。如果你用的是 OpenAI SDK,它会自动拼接/v1/chat/completions;如果你用的是 Anthropic SDK,它会拼接/v1/messages。
第三步是选模型。Symphony 的参考实现里默认用 Codex 系列模型做编码任务,但你可以根据实际需要切换。在 TaoToken 的模型列表里,常用的编码模型 ID 包括gpt-5-codex、claude-sonnet-4-5这类。我建议先用gpt-5-codex跑通流程,因为 Symphony 的规范里对 Codex App Server 的 JSON-RPC 接口有专门适配。
这里有个容易踩的坑:TaoToken 的 Base URL 是https://taotoken.net/api,但有些 SDK 默认会再加一层/v1,导致最终请求变成https://taotoken.net/api/v1/v1/chat/completions。解决办法是在初始化 SDK 时显式指定完整路径,或者把 Base URL 写成https://taotoken.net/api然后确认 SDK 的拼接逻辑。我试过用 OpenAI 的 Python SDK,直接传base_url="https://taotoken.net/api"是没问题的,它会正确拼成/api/v1/chat/completions。
如果你打算长期跑 Symphony 做编码任务,建议直接上 Coding Plan,因为编排器会持续不断地发起请求,按量计费在密集调用下成本不好控制。Coding Plan 的入口在 https://taotoken.net/coding-plan ,选好套餐后把 Key 配到环境变量里就行。
环境变量建议这样设置:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export SYMPHONY_MODEL="gpt-5-codex"把这三个变量写进你的 shell 配置文件(.zshrc或.bashrc),后面 Symphony 的配置里直接引用,避免把 Key 硬编码到代码里。
3. 可复制的 Symphony 配置:项目结构与 settings 片段
Symphony 的规范里对项目结构有明确约定,核心是三个文件:SPEC.md、WORKFLOW.md和一份编排器配置。SPEC.md定义问题和预期解决方案,WORKFLOW.md描述智能体应该遵循的工作流,编排器配置则告诉 Symphony 去哪里拉任务、用什么模型、怎么管理智能体生命周期。
先看目录结构。我建议按下面这样组织,这样后面排查问题时路径清晰:
symphony-local/ ├── config/ │ ├── orchestrator.toml │ └── agents.json ├── specs/ │ ├── SPEC.md │ └── WORKFLOW.md ├── workspaces/ │ └── (智能体工作空间,运行时自动生成) └── logs/ └── symphony.logorchestrator.toml是主配置文件,用 TOML 格式写。下面这份是我实测能跑通的配置,你可以直接复制后改几个字段:
[tracker] type = "linear" api_key_env = "LINEAR_API_KEY" team_id = "你的团队ID" poll_interval_seconds = 30 [agent] model = "gpt-5-codex" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" max_concurrent = 5 restart_on_crash = true workspace_root = "./workspaces" [workflow] spec_path = "./specs/SPEC.md" workflow_path = "./specs/WORKFLOW.md" auto_create_workspace = true [logging] level = "info" path = "./logs/symphony.log"几个关键字段说明一下。poll_interval_seconds控制 Symphony 多久扫一次看板,30 秒是个比较平衡的值,太短会频繁请求 Linear API,太长则新任务响应慢。max_concurrent限制同时运行的智能体数量,我建议从 3 开始,跑稳了再往上加,因为每个智能体都会独立调用模型,并发太高容易触发限流。restart_on_crash是 Symphony 的核心特性之一,智能体崩溃或卡住时自动重启,这个一定要开。
agents.json用来定义不同任务类型对应的智能体行为,比如编码任务和调研任务用不同的提示词模板:
{ "agents": [ { "name": "coder", "trigger_labels": ["code", "implementation"], "system_prompt": "你是一个编码智能体。从工单描述中提取需求,在独立工作空间中实现,完成后提交 PR 并附上变更说明。", "model": "gpt-5-codex", "max_turns": 50 }, { "name": "researcher", "trigger_labels": ["research", "spike"], "system_prompt": "你是一个调研智能体。分析代码库和相关文档,产出一份实施计划,不修改任何代码。", "model": "claude-sonnet-4-5", "max_turns": 20 } ] }这里有个设计要点:trigger_labels决定了工单上的哪个标签会触发哪个智能体。Symphony 的规范里强调「一一映射」,即每个未关闭的工单对应一个专属工作空间,但具体用哪个智能体来处理,是通过标签匹配的。这样你可以让同一个看板上的不同任务走不同的处理逻辑。
SPEC.md和WORKFLOW.md是给智能体看的,不是给人看的。SPEC.md描述项目要解决什么问题,WORKFLOW.md描述智能体应该按什么步骤操作。下面是一个WORKFLOW.md的最小示例:
# 工作流 ## 处理工单 1. 读取工单标题和描述,确认任务范围 2. 在独立工作空间中检出目标仓库 3. 创建分支,分支名格式:symphony/{issue_id}-{short_desc} 4. 实现变更,运行相关测试 5. 提交 PR,在 PR 描述中引用工单 ID 6. 将工单状态更新为「评审中」 ## 阻塞处理 如果工单被其他工单阻塞,等待阻塞解除后再开始。这份文件会被注入到智能体的系统提示里,所以写得越具体,智能体的行为越可控。我踩过的坑是:一开始WORKFLOW.md写得太抽象,智能体经常跳过测试直接提 PR,后来把「运行相关测试」这一步写死,问题就解决了。
4. 验证请求:确认 Symphony 编排行为符合预期
配置写完之后,别急着接真实看板,先用一个本地 mock 的工单源跑一遍,确认 Symphony 的编排逻辑是对的。这一步能帮你排除掉大部分配置错误,比直接接 Linear 调试效率高得多。
先启动 Symphony 的 dry-run 模式。参考实现里通常有个--dry-run参数,它会读取配置但不实际调用模型,只打印出「如果正常运行会做什么」:
symphony --config ./config/orchestrator.toml --dry-run预期输出应该类似这样:
[INFO] Loaded config from ./config/orchestrator.toml [INFO] Tracker: linear, team_id=xxx, poll_interval=30s [INFO] Agent: model=gpt-5-codex, base_url=https://taotoken.net/api [INFO] Workflow spec loaded: ./specs/SPEC.md (1240 bytes) [INFO] Workflow loaded: ./specs/WORKFLOW.md (680 bytes) [INFO] Dry run: would poll tracker every 30s, max 5 concurrent agents [INFO] Dry run complete. No agents started.如果这一步报错,大概率是配置文件路径不对或者环境变量没设置。检查TAOTOKEN_API_KEY和LINEAR_API_KEY是否在当前 shell 里可见:
echo $TAOTOKEN_API_KEY | head -c 8应该输出 Key 的前 8 个字符。如果输出为空,说明环境变量没生效,重新 source 一下配置文件。
dry-run 通过后,用一个本地 JSON 文件模拟工单源,验证智能体能否正确拉取任务并启动。创建一个mock-issues.json:
{ "issues": [ { "id": "TEST-001", "title": "添加用户登录接口的单元测试", "description": "为 auth/login.py 中的 login 函数补充单元测试,覆盖成功和失败两种情况。", "labels": ["code"], "state": "todo", "blocked_by": [] }, { "id": "TEST-002", "title": "调研现有日志框架的替换方案", "description": "分析当前使用的 logging 库,对比 structlog 和 loguru,产出一份对比报告。", "labels": ["research"], "state": "todo", "blocked_by": [] } ] }然后改一下orchestrator.toml里的 tracker 配置,临时指向这个文件:
[tracker] type = "local" issues_path = "./mock-issues.json" poll_interval_seconds = 5启动 Symphony:
symphony --config ./config/orchestrator.toml预期会看到类似这样的日志:
[INFO] Polling tracker: found 2 open issues [INFO] Issue TEST-001 matched agent 'coder', creating workspace... [INFO] Workspace created: ./workspaces/TEST-001 [INFO] Issue TEST-002 matched agent 'researcher', creating workspace... [INFO] Workspace created: ./workspaces/TEST-002 [INFO] Agent started for TEST-001 (coder, model=gpt-5-codex) [INFO] Agent started for TEST-002 (researcher, model=claude-sonnet-4-5)这时候去./workspaces/目录下看,应该有两个子目录,每个里面有一份工作空间初始化文件。如果智能体成功调用了模型,logs/symphony.log里会有对应的请求记录,包含请求的模型 ID 和响应状态。
验证模型调用是否真的走通了 TaoToken,可以单独发一个测试请求:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5-codex", "messages": [{"role": "user", "content": "回复 OK 两个字母"}], "max_tokens": 10 }' | head -c 200如果返回的 JSON 里有choices字段且内容包含OK,说明模型调用链路是通的。这一步很关键,因为 Symphony 的报错有时候会掩盖底层的 API 问题,单独验证一次能快速定位是编排层的问题还是模型层的问题。
5. 常见报错排查:401、local proxy failed 与 reading choices
跑 Symphony 的过程中,我遇到过几类典型报错,这里按出现频率排一下,每个都给出定位方法和修复动作。
401 Unauthorized是最常见的。报错长这样:
[ERROR] Agent TEST-001 failed: API request returned 401 {"error": {"message": "Invalid API key", "type": "invalid_request_error"}}原因通常是 Key 没设置对,或者环境变量没传到 Symphony 进程里。排查步骤:先确认echo $TAOTOKEN_API_KEY有输出,再确认orchestrator.toml里的api_key_env字段写的是TAOTOKEN_API_KEY而不是别的名字。如果都没问题,检查 Key 是否过期或被禁用,去 https://taotoken.net/api-keys 看一眼状态。还有一种情况是 Key 前面多了空格或换行,用echo -n输出确认一下。
local proxy failed这个报错比较隐蔽,通常出现在智能体尝试访问外部资源时:
[ERROR] Agent TEST-002 failed: local proxy failed: connection refusedSymphony 的参考实现里,智能体工作空间可能会配置一个本地代理来转发请求。如果代理进程没启动或者端口被占用,就会报这个错。解决办法是检查orchestrator.toml里有没有代理相关配置,如果有,确认代理进程在运行。我建议在本地开发时直接关掉代理配置,让智能体直连 TaoToken 的 API 端点,减少一层故障点。
reading choices这个报错说明请求发出去了,但响应格式不符合预期:
[ERROR] Agent TEST-001 failed: error reading choices: unexpected end of JSON input这通常是因为模型返回了空响应或者流式响应被截断。排查方法:先用上面那个 curl 命令单独测一次,确认模型能正常返回。如果 curl 正常但 Symphony 报错,检查 Symphony 用的 SDK 版本是否支持你选的模型。比如gpt-5-codex可能需要较新版本的 OpenAI SDK,旧版本可能不认识这个模型 ID,导致解析响应时出错。升级 SDK 到最新版通常能解决。
OAuth token expired如果你用的是需要 OAuth 的模型服务,可能会遇到这个:
[ERROR] Agent TEST-001 failed: OAuth token expired, please re-authenticateTaoToken 的 API Key 方式不涉及 OAuth,所以如果你看到这个报错,说明配置里可能混入了其他服务的鉴权逻辑。检查agents.json里有没有残留的 OAuth 配置,或者环境变量里有没有冲突的OPENAI_API_KEY之类的变量。清理掉这些干扰项,统一用TAOTOKEN_API_KEY就好。
智能体卡住不退出这个不算报错,但很影响体验。表现是日志停在某一步不再更新,工作空间里的文件也没变化。原因是智能体可能陷入了循环,反复尝试同一个操作。Symphony 的restart_on_crash只能处理进程崩溃,处理不了逻辑死循环。我的做法是在agents.json里给每个智能体设max_turns,比如 50 轮,超过就强制终止并标记工单为「需要人工介入」。这个字段在参考实现里不一定有,可能需要你自己在编排逻辑里加。
排查的时候有个通用技巧:把日志级别调到debug,能看到每次模型请求的完整 payload 和响应。在orchestrator.toml里改:
[logging] level = "debug" path = "./logs/symphony.log"debug 日志会比较大,排查完记得调回info。
6. 把 Symphony 接进你的日常:从验证到长期运行
本地跑通之后,下一步是把它接到真实的工单系统上。以 Linear 为例,你需要去 Linear 的设置里生成一个 API Key,然后把它设成LINEAR_API_KEY环境变量。团队 ID 在 Linear 的 URL 里能找到,格式是https://linear.app/你的团队/settings,把你的团队那段填到orchestrator.toml的team_id字段。
接真实看板之前,建议先在一个测试团队里跑一周。创建一个专门的 Linear 团队,建几个测试工单,观察 Symphony 的行为是否符合预期。重点看三件事:智能体是否正确识别了工单标签并匹配到对应的 agent;阻塞依赖是否生效,被阻塞的工单是否真的在等前置任务完成;PR 是否按WORKFLOW.md里定义的格式提交。
长期运行时,有几个参数需要根据实际情况调整。poll_interval_seconds如果设得太短,Linear API 可能会限流,我实测 30 秒是个安全值。max_concurrent取决于你的 TaoToken 套餐的并发限制,Coding Plan 的并发额度在控制台能看到,别超过那个数。max_turns要根据任务复杂度调,简单的代码修改 20 轮够了,复杂的功能开发可能需要 80 轮以上。
成本控制方面,Symphony 会持续不断地发起模型请求,即使没有新工单,它也会定期轮询。如果你用的是按量计费,建议设置一个每日预算上限,在 TaoToken 控制台里可以配。或者直接用 Coding Plan,包月套餐在密集调用下更划算。
最后说一个我踩过的坑:Symphony 的工作空间会随着工单数量增长而不断累积,每个工作空间都是一份完整的仓库检出,磁盘占用会涨得很快。建议加一个清理策略,比如工单关闭 7 天后自动删除对应的工作空间。这个逻辑参考实现里没有,需要你自己在编排层加一个定时任务。我是在orchestrator.toml里加了一个[cleanup]段,配置retention_days = 7,然后写了个简单的脚本每天跑一次。
如果你在接入过程中遇到配置问题,可以先去看接入文档 https://taotoken.net/doc ,里面有针对不同 SDK 的 Base URL 配置示例。模型调用本身有问题的话,用模型对话页面 https://taotoken.net/chat 单独测一下,能快速区分是编排层的问题还是模型层的问题。长期跑编码任务的话,Coding Plan 的入口在 https://taotoken.net/coding-plan ,选套餐的时候注意看一下并发限制和每日调用上限,这两个参数直接决定了你的 Symphony 能同时跑多少个智能体。