Prompts are code, .json/.md files are state

Treating LLMs as shitty general purpose computers we program with natural language. Because throwing shit at the wall wasn't working anymore.

mariozechner.at

本文探讨了在处理大型、成熟代码库时,如何通过将大语言模型(LLM)视为一种“性能较差的通用计算机”来提升 AI 代理(Agents)的工作效率。作者 Mario Zechner 指出,当前的“代理工程”往往缺乏工程严谨性,开发者通常只是向 AI 投喂信息并祈祷结果正确。在大型项目中,LLM 面临上下文缺失、缺乏审美(倾向于生成过度设计的代码)以及上下文降级(超过 100k token 后性能下降)等痛点。

为了解决这些问题,作者提出了一套方法论:提示词(Prompts)即代码,.json/.md 文件即状态。在这种模型下,“程序”是用自然语言编写的提示词,它定义了逻辑和控制流;“输入”是代码库文档和用户指令;“状态”则序列化存储在磁盘上的 JSON 或 Markdown 文件中,而非仅仅依赖不稳定的模型上下文。这种方式实现了工作流的可重现性和确定性。

文章以将 Spine 动画运行时从 Java 移植到 C++ 的真实案例展示了这一方法的威力。通过预先生成的 porting-plan.json 跟踪进度,并使用 port.md 作为执行脚本,作者引导 Claude Code 按照严格的步骤(查找待处理类型、打开相关文件、人工确认、执行移植、编译测试、更新状态)进行操作。这种结构化的方法将原本需要 2-3 周的人工移植工作缩短到了 2-3 天。作者强调,这种思维转变将 AI 辅助编程从一种随机的尝试转变为一种可控的工程学科。


主题 1:当前 AI 代理工程的痛点与局限性

在处理小型脚本或从零开始的项目时,像 Claude Code 或 Cursor 这样的工具表现出色。然而,一旦进入大型、已有的生产级代码库,问题便接踵而至。首先是上下文缺失:LLM 往往无法掌握项目的全貌,难以理解复杂的执行流,如多进程通信、并发架构或客户端-服务器交互。即使模型拥有巨大的上下文窗口,它们在处理超过 100k token 的信息时也会出现明显的“上下文降级”,丢失位于中间位置的关键细节。

其次,LLM 缺乏编程“品味”。由于是在全网代码上训练的,它们生成的代码往往是统计学上的平均水平。资深工程师追求简洁、优雅且易于维护的方案,而 LLM 则倾向于套用所谓的“最佳实践”,产出过度设计的垃圾代码,增加系统的复杂性和潜在 Bug。

最后,工具的不可控性也是一大障碍。许多商业工具为了节省 Token 成本,会私下削减用户的上下文,或者注入无法修改的系统提示词,导致模型无法获得必要的信息。作者认为,我们需要一种结构化的方式来“工程化上下文”,确保模型只获取任务所需的信息,减少工具调用次数,并实现尽可能高的确定性。

主题 2:将 LLM 视为“性能较差的通用计算机”

为了驯服 AI 代理的混乱,作者提出了一个核心隐喻:将 LLM 视为一台运行缓慢、不完全可靠的计算机。在这个框架下,我们可以应用传统的系统思维:

  • 程序(Program):即你的提示词。它用自然语言编写,规定了初始输入、通过工具描述“导入”外部函数,并实现业务逻辑(如顺序步骤、循环、条件判断甚至跳转)。
  • 输入(Inputs):包括预先准备的代码规范、架构概览、用户实时反馈以及工具的输出结果(如文件内容、命令执行结果)。
  • 状态(State):这是最关键的改变。作者主张将状态从易失的对话上下文中提取出来,序列化到磁盘。使用 JSON 存储结构化数据(利用 jq 进行精确读写),使用 Markdown 存储非结构化的小型数据。这样做的好处是,即使对话中断或达到上下文极限,你也可以随时从磁盘加载状态,开启全新的、干净的对话上下文继续工作。
  • 输出(Outputs):不仅是生成的代码,还包括 Diff 文件、代码库统计数据、变更摘要等。

这种“元编程”方法让开发者从“与 AI 聊天”转变为“编写在 LLM 上执行的逻辑”,从而利用结构化思维弥补自然语言的模糊性。

主题 3:实战案例:Spine 运行时的跨语言移植

作者以 Spine(2D 骨骼动画软件)的运行时移植为例。Spine 的参考实现是 Java,每当版本更新,都需要手动将其移植到 C++、C#、TypeScript 等十几种语言。这是一项极其枯燥、易出错且计算密集(涉及大量数学逻辑)的工作。

传统的移植流程依赖人工对比 Git Diff,手动分析依赖图,并逐行翻译。由于 Java 和 C++ 在内存管理、泛型和类型系统上存在巨大差异,人工移植往往在几小时后就会因大脑疲劳而引入 Bug。

作者通过编写一个名为 port.md 的“程序”改变了这一现状。他首先编写了一个脚本 generate-porting-plan.js,利用 LSP(语言服务器协议)分析 Java 和目标语言的代码库,生成一个详尽的 porting-plan.json。这个 JSON 文件记录了所有变更的类、它们所在的行号、依赖关系以及移植状态(pending/done)。这把一个开放式的探索问题变成了一个结构化的数据处理任务。LLM 不再需要自己去“寻找”该做什么,而是被告知“这是清单,按顺序执行”。

主题 4:构建结构化的工作流与工具链

在 Spine 项目中,port.md 定义了严密的执行逻辑。工作流分为几个阶段:

  1. 初始化:从 porting-plan.json 读取元数据,确定目标语言和路径。
  2. 约定与笔记:LLM 会读取(或生成)一份目标语言的编码规范(如命名规则、内存管理模式),并维护一份 porting-notes.md,记录移植过程中发现的特殊映射关系(例如:Java 的某个方法在 C++ 中必须改名以避免冲突)。
  3. 主循环
    • 使用 jq 查找下一个待处理的类型。
    • 通过 MCP(模型上下文协议)工具在开发者的 VS Code 中自动打开相关的 Java 和 C++ 文件。
    • 人工检查点:询问用户是否开始移植。
    • 执行移植:LLM 读取完整文件内容,根据规范进行翻译,并运行编译测试脚本。
    • 状态更新:移植完成后,LLM 使用 jq 更新 JSON 文件中的状态,并记录笔记。

这种方式确保了人类始终处于决策环路中(Human-in-the-loop),处理复杂的架构决策,而 LLM 则负责繁琐的代码转换和格式对齐。通过这种“程序化”的引导,原本需要数周的工作量被压缩到了几天内,且质量更高。


原文摘录

  • "Prompts are code, .json/.md files are state."(提示词是代码,.json/.md 文件是状态。)
  • "LLMs also lack taste... they generate the statistical mean of what they've seen."(LLM 缺乏品味……它们生成的是它们所见内容的统计平均值。)
  • "This is a weird form of metaprogramming: we write 'code' in the form of prompts that execute on the LLM to produce the actual code that runs on real CPUs."(这是一种奇怪的元编程:我们以提示词的形式编写“代码”,在 LLM 上执行,以产生运行在真实 CPU 上的实际代码。)
  • "The payoff? You can resume from any point with a fresh context, sidestepping the dreaded compaction issue entirely."(回报是什么?你可以从任何点带着全新的上下文重新开始,完全避开了可怕的上下文压缩问题。)
  • "What previously took me 2-3 weeks, now takes 2-3 days."(以前需要我花 2-3 周的时间,现在只需要 2-3 天。)

问答

问:为什么要把状态存储在 JSON 文件中,而不是直接留在对话历史里? 答:对话上下文是易失且有限的。随着对话增长,模型会发生“上下文降级”,丢失早期信息。将状态(如移植进度、配置)序列化到磁盘,可以让开发者随时开启新对话并加载状态,确保模型始终在干净、高效的上下文中工作,同时也实现了任务的可恢复性。

问:为什么不让 LLM 自己去探索代码库并决定移植顺序? 答:为了确定性和效率。LLM 自动探索容易遗漏文件、误解目录结构或陷入死循环。通过外部脚本(如利用 LSP)预先生成确定的移植计划,可以节省大量的 Token 和工具调用次数,确保任务覆盖 100% 的变更。

问:在这个流程中,人类的作用是什么? 答:人类充当“监视器”和“决策者”。在每个关键步骤(如开始移植新类、标记任务完成),程序都会设置人工检查点。人类负责处理 LLM 难以胜任的复杂架构决策(如 Java 泛型到 C++ 模板的转换),并审核生成的代码 Diff。

问:什么是 MCP,它在文中起到了什么作用? 答:MCP(Model Context Protocol,模型上下文协议)是一种允许 LLM 与外部工具交互的标准。文中作者使用它将 VS Code 变成 LLM 的“显示设备”,让 LLM 能主动为开发者打开文件、展示 Diff,从而实现紧密的人机协作。

问:这种方法适用于所有编程任务吗? 答:它最适合处理大型代码库中具有结构化、重复性特征的任务(如跨语言移植、大规模重构)。对于高度创新的绿地项目或简单的脚本编写,传统的交互式对话可能更快捷。