GPT-Pilot / Pythagora
一句话定位
GPT-Pilot 是一个把「从零构建一个全栈应用」拆解成 epics → tasks → steps → iterations 层级、由约 20 个专职 agent 接力完成的编码 harness。它的核心不是单条 ReAct 循环,而是一个在版本化项目状态之上运行的确定性有限状态机(FSM):下一个跑哪个 agent 完全由持久化的 ProjectState 决定,全程不使用任何原生 function calling,所有「工具」都是 prompt 内的 JSON / 标签 schema,由 pydantic 解析。
背景警示:本仓库 README 顶部标注「不再维护」(开发已转向闭源产品 pythagora.ai),且历史上出现过一次供应链投毒事件(见「安全与权限」章)。本 dossier 分析的是清理后的 HEAD。
核心架构总览(目录结构关键路径 + 引用的 commit)
- 分析 commit:
9b763fdaf0020c7d8abacc7b58b2b09e57494623(2026-06-12 09:28:32 +0200,合并 PR #1183「remove-malicious-telemetry-loader」)。这是 pilot v2 重写版,代码全部位于core/(v1 曾在pilot/,仅剩core/db/v0importer.py+core/config/env_importer.py兼容导入旧项目/.env)。 - 语言栈:Python 3 async + SQLAlchemy/Alembic + Jinja2 + pydantic。Node/React 是目标产物的技术栈,不是 harness 本身。
- 关键目录:
core/agents/— 约 20 个BaseAgent子类(专职 agent)+ 编排器orchestrator.pycore/state/state_manager.py— 版本化状态持久化(copy-on-write 的ProjectState链)core/llm/— provider 分发、无 function-calling 的消息适配、JSON schema 解析与重试core/prompts/<agent_type>/+core/prompts/partials/— 每 agent 一套 Jinja 模板core/proc/process_manager.py— 宿主机子进程执行(无沙箱)core/telemetry/— 可选 opt-in 遥测(清理后仅剩合法__init__.py)
- 一段话架构:
Orchestrator.create_agent()读持久化状态返回下一个专职 agent;该 agent 运行、在一份 copy-on-write 的next_state上做修改、返回AgentResponse;编排器把状态 commit 进 SQLite 再循环。
Agent Loop(主循环 / 何时继续何时停)
- 主循环在
core/agents/orchestrator.pyOrchestrator.run()L48-167:单个while True:。每轮:redo 时重载 →update_stats()→agent = self.create_agent(response)→agent.run()→ 按response.type分派。 - 停止条件:外层循环只在
ResponseType.EXIT时 break(L149)。ResponseType.DONE走handle_done(),commit 状态后continue(L153-164)。外层没有 max-iteration 上限——推进由状态迁移保证,而非计数器。 - 真正的「路由大脑」是
create_agent()L463-552:一条很长的if/elif阶梯,对current_state(epics/spec/architecture/tasks/steps/iterations)判定该跑哪个 agent。跑哪个 agent 是持久化状态的纯函数。 - step 级分派:
create_agent_for_step()L554-573,把某个 step 的type映射到 agent(save_file→CodeMonkey,command→Executor,human_intervention→HumanInput,create_readme→TechnicalWriter 等)。 - 有限并行:L109-139,当
create_agent()返回列表(目前仅save_file/CodeMonkey)时用asyncio.gather并行;handle_parallel_responses()L345-368 只会合并 CodeMonkey 结果,否则抛异常。 - agent 内部还有各自的重试小循环:Developer breakdown 标签补全循环
developer.pyL268-282(max_retries=2);CodeMonkey reviewMAX_CODING_ATTEMPTS=3(code_monkey.pyL31);bug-hunt 对话轮次上限CONVO_ITERATIONS_LIMIT(bug_hunter.py引入)。
记忆与上下文管理(压缩、长期记忆、会话持久化)
- 持久化 = DB 里的版本化状态链,而不是 chat log。
core/state/state_manager.py:StateManager持有只读current_state+ 可写next_state。commit()L433-489 保存next_state,每步关闭并重开 SQLAlchemy session,再create_next_state()fork 出一份新的可变状态。完整会话可恢复:load_project()L272 按branch_id/step_index加载,restore_files()L740 从 DB 重写整个工作区。 - DB:默认 SQLite(
sqlite+aiosqlite:///data/database/pythagora.db,config/__init__.pyDBConfig ~L385),可选 PostgreSQL;Alembic 迁移在core/db/migrations。 - 无向量库 / 无语义长期记忆。 「记忆」是结构化的:
ProjectState携带epics/tasks/steps/iterations/files(含每文件content.meta.description+references)和一个knowledge_base(pages/apis/user_options/utility_functions,orchestrator L75-82)。相关性靠显式按任务挑选而非 embedding 检索:Developer.get_relevant_files_parallel()(RelevantFilesMixin)在 breakdown 前让 LLM 判定哪些文件相关。 - 上下文压缩粗糙且写死。
core/llm/base.py__call__L167-182:若估算 prompt token > 150000,就找最后一条「Here are the backend/frontend logs」消息,对它之前的内容跑trim_logs()(core/utils/text.py)。这是唯一的自动压缩。此外AgentConvo.trim/slice/remove_last_x_messages(agents/convo.pyL91-135)是 agent 手动调用的编辑助手。 - 每个 agent 每次调用都新建一份对话
AgentConvo(agent)(agents/convo.py),system prompt 每次重渲染(L27-31)。跨 agent 没有持续增长的 chat buffer——历史从状态重建。
工具体系(定义/调用协议/注册/权限)
- 无 function-calling 协议。
core/llm/base.py_adapt_messagesL98-122 直接raise ValueError("Anthropic Claude doesn't support function calling"),把角色塌缩为 user/assistant。整个代码库靠 prompt 指令下的结构化输出驱动工具。 - 「工具集」= 一个 pydantic discriminated union of step types,
core/agents/developer.pyL32-82:StepType枚举:command/save_file/human_intervention/utility_function。- schema:
CommandStep{command, timeout, success_message}、SaveFileStep{path}、HumanInterventionStep{description}、UtilityFunction{...};union =Step,外层TaskSteps{steps:[...]}。
- 调用协议: LLM 产出符合 schema 的 JSON。
AgentConvo.require_schema()(agents/convo.pyL112-128)把解引用后的 JSON schema 注入 prompt 并要求「your response MUST conform」,回复由JSONParser(spec=...)(core/llm/parser.py)解析。以自然语言定义工具语义的 prompt 是core/prompts/developer/parse_task.prompt(含 command/save_file/human_intervention 语义与示例)。 - 文件写入「工具」用的是第二套、基于标签的协议(非 JSON): Developer breakdown 把文件正文放进
<pythagoracode file="path">...</pythagoracode>标签,由正则extract_code_blocks()(code_monkey.pyL61-65)抽取,has_correct_num_of_tags()(developer.pyL84-88)做标签配平检查。 - 注册 = 编排器里写死的分派,而非插件注册表。
create_agent_for_step()(orchestrator L554-573)是唯一把 step 类型绑到执行器的地方。 - 执行权限门:
core/agents/executor.pyExecutor.run()L86-102——每个commandstep 运行前弹用户确认(ask_questionyes/no,命令文本可编辑),选「no」则跳过。详见「安全与权限」。
Prompt 设计(系统提示结构、动态组装)
- 每 agent 一套 Jinja2 文件模板,位于
core/prompts/<agent_type>/,共享片段在core/prompts/partials/。加载器JinjaFileTemplate(core/llm/prompt.pyL33-45),StrictUndefined,autoescape 关闭。 - 组装:
AgentConvo.__init__(agents/convo.pyL23-31)构造时自动渲染<agent_type>/system.prompt作为 system message;.template(name, **kw)L74-83 渲染<agent_type>/<name>.prompt追加为一个 user turn,同时记录结构化prompt_log(模板名 + 序列化上下文)供追溯。 - 默认注入变量(
_get_default_template_varsL41-52):state(整个current_state)和os(Windows/macOS/Linux),因此 prompt 是状态感知 + OS 感知的;如 parse_task 会说「must run on a {{ os }} machine」。 - system prompt 简短、带角色色彩(
developer/system.prompt:「You are a world class full stack software developer…」)。厚重的行为规则放在 partials 里用{% include %}拉入,如partials/coding_rules.prompt(7+ 条编号规则:整文件重写格式、日志、错误处理、INPUT_REQUIRED {desc}密钥占位约定、前端错误规则受state.has_frontend()门控)、partials/human_intervention_explanation.prompt、partials/execution_order.prompt。 require_schema()对任意 pydantic 类型化输出,额外挂一条 JSON-schema 约束消息到对话上。
Router / 编排(任务分解、多 agent、子 agent)
- 这是本 harness 的定义性特征:
core/agents/下约 20 个专职 agent,都是BaseAgent子类(有agent_type/display_name/run()),由 dim1 的 FSM 编排。角色表(文件 → 职责):wizard.pyWizard(初始项目设置)、spec_writer.pySpecWriter(细化 spec)、architect.pyArchitect(架构 + 依赖)、tech_lead.pyTechLead(epic→task 分解、套模板)、developer.pyDeveloper(task→steps 分解)、code_monkey.pyCodeMonkey(写/审单个文件)、executor.pyExecutor(跑 shell 命令)、troubleshooter.pyTroubleshooter(运行/复核、bug 报告)、bug_hunter.pyBugHunter(日志驱动调试循环)、problem_solver.pyProblemSolver(「卡循环」升级)、error_handler.pyErrorHandler、human_input.pyHumanInput、importer.pyImporter、frontend.pyFrontend、external_docs.pyExternalDocumentation、tech_writer.pyTechnicalWriter、task_completer.pyTaskCompleter、legacy_handler.pyLegacyHandler。
- 分解层级: epics → tasks → steps → iterations。TechLead 把 epic 拆成 task;
Developer.breakdown_current_task()(developer.pyL204-311)把一个 task 转成类型化 steps(两阶段:先流式自由文本 breakdown,再单独一次parse_taskLLM 调用转成TaskStepsschema)。 - 子 agent / 并行: 仅 CodeMonkey 对并行
save_filesteps 做同类型 fan-out(orchestrator L109-139、L554-561);相关文件查找也跑并行 LLM 调用(mixins)。没有通用的嵌套子 agent 派生。 - 共享行为靠 mixins(
core/agents/mixins.py):ChatWithBreakdownMixin(接受前让用户与拟定 breakdown 对话)、RelevantFilesMixin、FileDiffMixin。
Skill / 插件体系
- 未实现为可扩展体系。 无 skill/plugin 加载器、无 entry-points、无动态工具发现。最接近的三个类比:
- 项目模板(
core/templates/tree/...,如vite_react)由 TechLead 套用——是脚手架,不是运行时 skill。 utility_functionstep 类型(developer.pyL64-71,update_knowledge_base()L478-484)——agent 把可复用 helper 函数登记进项目knowledge_base,是项目局部的能力备忘,不是 harness 插件。- 外部文档抓取(
external_docs.py,config 里EXTERNAL_DOCUMENTATION_API)——为已知库拉取文档片段,是固定集成,不可由用户插拔。
- 项目模板(
- 扩展 harness = 改 Python + prompt 文件。此维度记为未实现 / N/A。
自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)
- 无权重/prompt 自我改进,无跨项目持久学习。 但存在强的会话内 eval 驱动纠错循环:
BugHunter(core/agents/bug_hunter.py)驱动日志埋点式调试循环。check_logs()L103+ 通过魔法词让 LLM 判定问题是否已定位或需更多日志(HuntConclusionType:ADD_LOGS/PROBLEM_IDENTIFIED,L31-39)。若需更多日志,Developer 插入 logging、用户复现、循环重复(IterationStatus状态机在 orchestrator L518-549)。- 命令结果由 LLM 判定:
Executor.check_command_output()(executor.pyL147-167)→CommandResult{analysis, success}决定任务继续还是触发调试。(注意 L132if True or llm_response.success:失败→ErrorHandler 分支目前被死代码短路成常真,是已知 FIXME。) - CodeMonkey 做自我 code-review:
ReviewChanges/Hunk{decision: apply|ignore|rework}(code_monkey.pyL35-49)逐 hunk 审自己的 diff,最多MAX_CODING_ATTEMPTS=3次 rework。 ProblemSolver在用户报告卡住时升级;telemetry.trace_loop()记录循环情形。
- 上述任何一环都不回流到模型或持久 skill 库。
可观测性(日志 / trace 格式)
- 结构化 Python logging:
core/log/get_logger;LogConfig(config/__init__.py~L208)默认落data/pythagora.log,级别 DEBUG,带max_lines上限。编排器/agent 记录每次迁移(如 orchestrator L142「Running agent … (step N)」)。 - 逐请求 LLM 日志: 每次调用产生一条
LLMRequestLog(core/llm/request_log.py),含 provider/model/temperature/messages/response/tokens/duration/status/error,在base.py __call__创建、由StateManager.log_llm_request()(state_manager.pyL491-523)持久化——但仅当db.save_llm_requests为真(默认 False);遥测计数器则始终记录。 - 命令执行日志:
ExecLog(core/proc/exec_log.py),含 cmd/cwd/timeout/status/stdout/stderr/analysis/success,经log_command_run()(state_manager.pyL539)保存;用户输入经log_user_input()L524 保存。 - Phone-home 遥测:
core/telemetry/__init__.py——单例、opt-in(settings.telemetry.enabled,可用DISABLE_TELEMETRYenv 关)。聚合计数(num_llm_requests/errors/tokens、num_steps/commands/inputs、end_result、大/慢请求统计)POST 到配置的 endpoint;另有trace_code_event()L363 /trace_loop()记命名事件(如trace-task-start,developer.py L303)。崩溃诊断(record_crashL214)截取最内层 3 个core/栈帧。 - UI 流式 trace: 每条 agent 消息/提问/流式 chunk 都打上
AgentSource(display_name, agent_type)和project_state_id(agents/base.pysend_message/stream_handlerL56-141),使 VSCode 扩展能把输出归因到具体 agent 与状态版本。
安全与权限(审批门、密钥管理)
- 命令审批门:
Executor.run()(executor.pyL86-102)——任何 shell 命令都必须经用户 yes/no 确认(命令文本可编辑)才运行。这是主要安全边界。RUN_COMMAND常量来自core/config/actions.py。 - Git 审批门:
GitMixin(agents/git.py)在git init、每次 commit 前询问,并展示 LLM 生成的 commit message 供接受/编辑/拒绝。没有静默提交。 - 密钥的人工输入门:
INPUT_REQUIRED {config_description}注释约定(定义在partials/coding_rules.prompt规则 4)——模型在 config/.env 文件里标记需用户填写的行(API key 等),而不是自己编造。StateManager.get_input_required()(state_manager.pyL1014-1028)扫描已保存文件里的该标记,命中即路由到HumanInputagent(orchestrator L471-473,AgentResponse.input_required)。凭据被交还给人,而非虚构。 - 密钥管理(harness 自身的 key): LLM API key 走 JSON config 里的
ProviderConfig.api_key,未设则用 provider env(config/__init__.pyL102-105);旧.env由config/env_importer.py导入。可选运行时access_token(Pythagora/Bricks 代理)逐请求注入为Authorization: Bearer(llm/base.pyL217-249),经state_manager.update_access_token(agents/base.pyL120-122)存储。无 prompt 的密钥扫描/脱敏。 - 离线改动安全: 启动时
offline_changes_check()(orchestrator L370-416)检测带外编辑,询问导入还是覆盖。 - 供应链事件(本维度头号发现): README L1-16 的
[!CAUTION]块记录:恶意 commit065ee8eb(伪装成「Revert ‘Implemented weekend discount’」)于 2025-08-24 推入、2026-06-08 被公开报告、2026-06-11 删除、2026-06-12 清理合并至 HEAD。payload 是隐藏加载器core/telemetry/_hooks.py(由core/telemetry/__init__.py自动启动),下载 Bun 运行时执行混淆的core/telemetry/_runtime.bin——一个 Shai-Hulud 类窃密蠕虫(收割云/AWS key、GitHub/npm token、SSH key)。HEAD 已验证清洁:core/telemetry/现仅剩__init__.py,grep_hooks|_runtime|loader.lock|bun.sh无加载器命中;但CHANGELOG.md仍留着攻击者伪装的 revert 条目(日期 2025-08-25)。项目「不再维护 + 一条恶意 commit 潜伏约 10 个月」这一安全姿态本身,是本维度真正的结论。
沙箱与执行隔离
- 无。命令直接在宿主机运行。
core/proc/process_manager.pyLocalProcess.start()L38-62:asyncio.create_subprocess_shell(cmd, cwd=..., env=...)——在项目根目录的裸 shell,继承完整环境(ProcessManager.__init__L149-150env = deepcopy(environ))。无容器、无 VM、无 seccomp、无降权、无网络/文件系统限制。 - 唯一护栏:用户审批门(见「安全与权限」)+ 超时——
MAX_COMMAND_TIMEOUT = 180s 硬上限(process_manager.pyL21,L240 强制),超时则整棵进程树 SIGKILL(_terminate_process_treeL106-126,用 psutil)。 - 后台/前台进程管理 + 非阻塞输出流由
watcher()协程(L171-200)实现。仓库有Dockerfile但那是容器化整个 app 用于部署,agent 跑的命令相对于 harness 并未被沙箱化。
与模型的协同设计
- provider 无关、逐 agent 的模型路由。
AgentLLMConfig把每个 agent 名映射到 provider+model+temperature;Config.llm_for_agent()(config/__init__.pyL423-434)在没有专属配置时回退到"default"agent 配置。细粒度 agent key(config/__init__.pyL38-53,如CodeMonkey.code_review、CodeMonkey.implement_changes、Developer.parse_task、BugHunter.check_logs)让不同子任务指向不同模型/温度。 - Provider(
LLMProviderL70-81):OpenAI、Anthropic、Groq、Azure、LM-Studio,以及 Relace(专用 fast-apply 合并模型)。client 类由BaseLLMClient.for_provider()(llm/base.pyL415-440)选择。 - Relace fast-apply 协同: CodeMonkey 先用 Relace 把编辑片段合并进文件(
code_monkey.pyL118-133,IMPLEMENT_CHANGES_AGENT_NAME、relace_client.py、OptionalCodeBlockParser),Relace 不可用才回退到 OpenAI 整文件重写——一个规划模型与廉价合并模型的明确分工。 - 代码里默认模型偏旧(
AgentLLMConfig.model默认gpt-4o-2024-05-13,L131;遥测默认亦然),反映项目 2024 年的冻结。 - 因为没有 function-calling,设计刻意通过 JSON-schema-in-prompt + 稳健解析/重试面向任意 chat 模型(
llm/base.pyL389-402:解析 ValueError 时把错误回喂并重试至max_retries)。
轨迹利用(session/trajectory 是否反哺训练/评测)
- 无训练/RL/eval 循环。 全树无数据集导出、无 reward、无 fine-tuning 钩子。
- 但轨迹完整持久化且可回放,用于工程/调试:版本化
ProjectState链(branch_id + step_index)是完整可恢复的构建 trace(见「记忆」章);可选save_llm_requests在 DB 存下每条 prompt/response(见「可观测性」章);get_task_conversation_project_states()为 UI「回看日志」面板重建某任务的对话(developer.pyL377-390)。 - 唯一外流是聚合 opt-in 遥测(见「可观测性」章)到 Pythagora endpoint——计数 +
trace-*事件 + 崩溃栈帧,而非供模型训练的原始轨迹。
与同类 harness 的关键差异(1-3 条)
- 确定性 FSM 而非 ReAct 循环:跑哪个 agent 是持久化状态的纯函数(
orchestrator.create_agent()),外层循环无 max-iteration 计数,靠状态迁移保证推进——与大多数「LLM 自己决定下一步调什么工具」的 agent 截然不同。 - 全程零 function-calling:
llm/base.py显式拒绝 function calling,所有工具/文件写入都是 prompt 内的 JSON schema 或<pythagoracode>标签 + pydantic 解析 + 出错回喂重试,因此可对接任意 chat 模型(含本地 LM-Studio)。 - 约 20 个高度专职 agent + epics→tasks→steps→iterations 层级分解:把「造一个 app」拆成软件团队角色(SpecWriter/Architect/TechLead/Developer/CodeMonkey/BugHunter…),并用 Relace fast-apply 做规划/合并模型分工——比通用单 agent coding harness 更像流水线。
原始源码定位
- repo: https://github.com/Pythagora-io/gpt-pilot
- commit/version analyzed:
9b763fdaf0020c7d8abacc7b58b2b09e57494623(2026-06-12,pilot v2 重写版,代码全在core/;README 标注仓库不再维护) - 关键文件列表(相对仓库根):
core/agents/orchestrator.py— FSM 路由 + 主循环(run()L48-167、create_agent()L463-552、create_agent_for_step()L554-573)core/agents/base.py— BaseAgent、get_llm、ask_question、stream/error handlers(L56-141)core/agents/developer.py— task→steps、StepType/TaskSteps工具 schema(L32-82)、breakdown(L204-311)core/agents/code_monkey.py—<pythagoracode>写文件协议(L61-65)、自我 code-review(L35-49)、Relace 合并(L118-133)core/agents/executor.py— 命令审批门(L86-102)、LLM 判定命令结果(L147-167)core/agents/bug_hunter.py— 日志驱动调试循环、魔法词(L31-39)core/agents/convo.py— AgentConvo、require_schema(L112-128)、trim/slice(L91-135)core/agents/git.py— git 审批门(GitMixin)core/llm/base.py— 无 function-calling(L98-122)、token-trim 压缩(L167-182)、provider 分发(L415-440)、重试(L389-402)core/llm/prompt.py、core/llm/convo.py— Jinja 模板core/proc/process_manager.py— 宿主机 subprocess 执行、180s 超时、无沙箱(L21/L38-62/L106-126)core/config/__init__.py— LLMProvider、逐 agent 模型路由(L423-434)、DB/log 配置core/telemetry/__init__.py— opt-in phone-home 遥测(已验证清洁)core/state/state_manager.py— 版本化状态持久化(commit L433-489、load L272、restore L740、INPUT_REQUIRED 扫描 L1014-1028)- prompts:
core/prompts/developer/system.prompt、.../parse_task.prompt、core/prompts/partials/coding_rules.prompt、.../human_intervention_explanation.prompt
一手源存档(sources/)
存档于 /Users/zhao/projects/self-wiki/ai-research/sources/harness/gpt-pilot/(约 260K):
NOTES.md— Phase-1 逐维度源码级笔记(含 provenance、供应链事件、12 维证据与文件行号)src/— 从 clone 复制的关键源码子集:src/agents/:base.py、bug_hunter.py、code_monkey.py、convo.py、developer.py、executor.py、git.py、orchestrator.pysrc/llm/:base.py、convo.py、prompt.pysrc/state/state_manager.pysrc/proc/process_manager.pysrc/config/config_init.pysrc/telemetry/telemetry_init.pysrc/prompts/:system.prompt、parse_task.prompt、coding_rules.promptsrc/README-security-notice.md(README 安全通告留档)