OpenAI 刚开源了 Symphony——不是又一个模型,不是又一个框架,而是一份 SPEC.md。它定义了一套用项目管理工具(Issue Tracker)做控制面的多 Agent 编排方案:开发者不再逐个盯交互式编码会话,而是把每个 Issue 交给一个专属 Agent,Agent 自主跑完任务,人只做最终 Review。思路很朴素,但把"谁来管 Agent"这个问题从人推给了流程。
从"人盯 Agent"到"流程管 Agent"
当前主流的编码 Agent 工作方式是交互式:你在终端或 IDE 里开一个会话,Agent 写代码,你审查、追问、修正,循环往复。一个人能盯的会话数量有限,Agent 的并行能力被卡死在人的注意力带宽上。
Symphony 的核心翻转是——把 Issue Tracker 当成控制面。Issue 本身就是任务的原子单位:有描述、有状态、有依赖关系。Symphony 读 Issue 列表,为每个 Open Issue 派一个 Agent,Agent 拿到 Issue 描述后自主编码、跑测试、提交 PR,最后把 Issue 标记为 Done。人只在 PR Review 阶段介入。
这意味着: - Agent 的并行度不再受人的注意力限制,受的是 Issue 数量和 CI 资源。 - 人的角色从"实时驾驶员"变成"批量质检员"。 - 项目管理工具天然就是状态机,不需要额外造一套编排 DSL。
SPEC.md 里定义了什么
Symphony 目前开源的是 SPEC.md,不是可运行的代码。这份规格文档描述了编排的约定和流程,核心要素包括:
| 要素 | 作用 |
|---|---|
| Issue 作为 Task 单元 | 每个 Issue 对应一个 Agent 的完整任务生命周期 |
| Agent 的自主边界 | Agent 在 Issue 描述范围内自主决策,不回问人 |
| 状态流转 | Open → In Progress → Done,由 Agent 和 Tracker 共同驱动 |
| Human Review Gate | Agent 完成后产出 PR,人做 Review,不通过则打回重开 Issue |
关键设计决策:Agent 不做交互式确认。它拿到 Issue 描述后一路跑到终点,中间不暂停、不请求许可。这和 Codex / Copilot 的"对话式"模式截然不同。代价是 Issue 描述必须足够精确——模糊的 Issue 会产出模糊的 PR。
实际跑起来:一个最小编排原型
Symphony 的 SPEC 是规范,不是实现。但我们可以用它的思路搭一个最小原型——用 GitHub Issue 做 Tracker,用 Python 脚本做 Orchestrator,用 OpenAI API 做 Agent 核心。下面是一个可以直接改造运行的示例。
1. 定义 Issue 模板
在你的 repo 里创建 .github/ISSUE_TEMPLATE/agent_task.yml:
name: Agent Task
description: 一个可由编码 Agent 自主完成的任务
labels: ["agent-task"]
body:
- type: textarea
id: description
attributes:
label: 任务描述
description: 请写清楚要做什么、改哪个文件、期望的行为是什么。Agent 不会回问。
placeholder: "例如:在 src/api/users.py 中添加 DELETE /users/{id} 端点,返回 204,需覆盖测试"
validations:
required: true
- type: textarea
id: constraints
attributes:
label: 约束与边界
description: Agent 必须遵守的规则
placeholder: "例如:不要改动数据库 schema;只修改 users.py 和 tests/test_users.py"
2. Orchestrator 脚本
这个脚本轮询带 agent-task 标签的 Open Issue,为每个 Issue 启动一个 Agent 运行:
"""
symphony_minimal.py — 最小 Symphony 编排原型
依赖: pip install openai PyGithub
环境变量: GITHUB_TOKEN, OPENAI_API_KEY, REPO_NAME (格式: owner/repo)
"""
import os
import json
import subprocess
from github import Github
from openai import OpenAI
REPO_NAME = os.environ["REPO_NAME"] # e.g. "myorg/myrepo"
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
gh = Github(GITHUB_TOKEN)
repo = gh.get_repo(REPO_NAME)
client = OpenAI(api_key=OPENAI_API_KEY)
AGENT_LABEL = "agent-task"
STATUS_LABELS = {"in-progress": "agent-in-progress", "done": "agent-done"}
def fetch_open_tasks():
"""获取所有标记为 agent-task 且未关闭的 Issue"""
issues = repo.get_issues(state="open", labels=[AGENT_LABEL])
return [i for i in issues if not any(
l.name == STATUS_LABELS["in-progress"] or l.name == STATUS_LABELS["done"]
for l in i.labels
)]
def mark_status(issue, status):
"""给 Issue 打上状态标签"""
label = repo.get_label(STATUS_LABELS[status])
issue.add_to_labels(label)
def agent_run(issue):
"""让 Agent 自主完成一个 Issue"""
prompt = f"""你是一个自主编码 Agent。以下是你的任务 Issue:
标题: {issue.title}
描述: {issue.body}
规则:
1. 只修改与任务直接相关的文件。
2. 写完代码后生成对应的测试。
3. 输出格式为 JSON,包含两个字段:
- "files": 一个 dict,key 是文件路径,value 是文件完整内容
- "summary": 一段简短的变更说明
请直接输出 JSON,不要解释过程。"""
response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
result = json.loads(response.choices[0].message.content)
return result
def apply_changes(result, issue):
"""把 Agent 产出的文件写入本地,提交并推 PR"""
branch_name = f"agent-task-{issue.number}"
base_branch = repo.default_branch
# 从 base branch 创建新分支
ref = repo.get_git_ref(f"heads/{base_branch}")
repo.create_git_ref(f"refs/heads/{branch_name}", ref.object.sha)
for path, content in result["files"].items():
# 尝试获取已有文件(如果存在则更新,否则创建)
try:
existing = repo.get_contents(path, ref=branch_name)
repo.update_file(
path=path,
message=f"agent update: {path}",
content=content,
sha=existing.sha,
branch=branch_name,
)
except Exception:
repo.create_file(
path=path,
message=f"agent create: {path}",
content=content,
branch=branch_name,
)
# 创建 Pull Request
pr = repo.create_pull(
title=f"[Agent] {issue.title}",
body=result["summary"] + f"\n\nCloses #{issue.number}",
head=branch_name,
base=base_branch,
)
# 在 Issue 上评论 PR 链接
issue.create_comment(f"Agent 已完成,PR: #{pr.number}")
return pr
def main():
tasks = fetch_open_tasks()
print(f"发现 {len(tasks)} 个待处理 Agent Task")
for issue in tasks:
print(f"→ 处理 Issue #{issue.number}: {issue.title}")
mark_status(issue, "in-progress")
result = agent_run(issue)
pr = apply_changes(result, issue)
mark_status(issue, "done")
print(f" ✓ PR #{pr.number} 已创建,等待人类 Review")
if __name__ == "__main__":
main()
3. 运行方式
# 安装依赖
pip install openai PyGithub
# 设置环境变量
export GITHUB_TOKEN="ghp_xxxx"
export OPENAI_API_KEY="sk-xxxx"
export REPO_NAME="myorg/myrepo"
# 执行编排
python symphony_minimal.py
这个原型做了 Symphony SPEC 里描述的核心流程:读 Issue → 派 Agent → Agent 自主产出 → 提 PR → 等 Review。你可以在此基础上加 CI 检查、依赖排序、失败重试。
人的角色变了,没消失
Symphony 把人从"实时驾驶员"挪到了"批量质检员",但这不意味着人可以撒手不管。几个实际要注意的点:
Issue 质量就是 Agent 质量。 Agent 不回问,所以 Issue 描述必须精确到文件路径、函数名、期望行为。模糊的 Issue 会产出无用的 PR,Review 成本反而更高。建议团队先花时间打磨 Issue 模板和写作规范,再放开 Agent 自动跑。
Review 是最后的闸门,不是可选步骤。 Agent 产出的 PR 必须经过完整 Review——代码逻辑、测试覆盖、边界情况。如果团队习惯快速 merge 小 PR,Agent 产出的 PR 量可能压垮现有 Review 流程。需要考虑自动化 lint + CI gate 作为前置过滤,人只 Review 通过 CI 的 PR。
依赖顺序不能忽略。 如果 Issue A 依赖 Issue B 的产出,并行派 Agent 会冲突。SPEC.md 里暗示用 Issue 的依赖关系做排序,但 GitHub Issue 本身没有原生依赖字段。你可以用标签(如 depends-on:#123)或项目管理工具(如 Linear、Jira)来表达。
失败处理要提前设计。 Agent 跑到一半 CI 爆了、文件冲突了、API 超时了——这些都要有明确的状态回退路径。最简单的做法:失败时把 Issue 标回 Open,评论里附上错误日志,让人决定是重派 Agent 还是手动修。
什么时候值得用
Symphony 的模式适合以下场景:
- Issue 量大、单个复杂度低——比如批量修 lint warning、批量加类型标注、批量补测试。这些任务描述容易标准化,Agent 成功率高。
- 团队已经有成熟的 Issue + PR 流程——Symphony 是在现有流程上加自动化层,不是替代流程。如果团队连 Issue 都不写,先解决流程问题。
- CI 基础设施可靠——Agent 产出的代码必须过 CI 才值得人 Review。CI 不稳,Agent 的产出就是噪音。
不适合的场景:
- 探索性任务——"调研一下这个新框架"这类 Issue,Agent 没有明确的完成标准。
- 跨多模块的大重构——依赖关系复杂,并行 Agent 容易互相踩脚。
- 对正确性要求极高的变更——比如支付逻辑、安全补丁,Agent 自主跑完再 Review 的风险太高。
Symphony 的 SPEC.md 是一份设计文档,不是产品。它的价值在于给出了一个清晰的编排约定:用 Issue Tracker 做状态机,用 Agent 做执行器,用 PR Review 做安全网。你可以用任何语言、任何模型、任何 Tracker 来实现这个约定——上面那个 Python 原型就是起点。