构建 AI 驱动的代码编辑器:(第2部分)类似 LLM 的解释器
Source: Dev.to
(请提供需要翻译的正文内容,我将为您翻译成简体中文并保持原有的格式。)
Source: …
洞察
在构建 LLM CodeForge(一个让 LLM 能够自主读取、修改和测试代码的代理编辑器)时,我在写了 5 000 个 token 的指令后意识到:
我并不是在写提示词。我在构建一种嵌入自然语言中的 领域特定语言(DSL)。
本文分析了这种区分为何根本重要——以及你可以从中为自己的代理系统学到什么。
模型在 CodeForge 中的行为
| 方面 | 模型 会做 的事 | 模型 不会做 的事 |
|---|---|---|
| 决策制定 | 选择 协议的哪个分支进行 | 不决定 要做什么 |
| 问题求解 | 执行 用自然语言描述的过程 | 不 创造性地解决问题 |
| 本质 | 像 字节码解释器、文本驱动的有限状态机,或 具有封闭动作的规划器 | 依赖确定性的控制流,而非开放式推理 |
| 可靠性 | 因为我 接受 LLM 本质上不可靠而工作 | N/A |
DSL 控制流
每个请求遵循 四 步:
[UNDERSTAND] → [GATHER] → [EXECUTE] → [RESPOND]
第 1 步 – UNDERSTAND
分类请求类型
| 类型 | 关键词 | 下一步 |
|---|---|---|
| 解释 | “what is”, “explain” | [RESPOND] (text) |
| 修改 | “add”, “change” | [GATHER] → [EXECUTE] |
| 分析 | “analyze”, “show” | [GATHER] → [RESPOND] |
这 不是 经典意义上的链式思考。这是确定性的任务路由——一个将 输入 → 工作流 的决策表。模型并不“思考”;它执行 条件跳转。
不变规则
🚨 关键规则:您 不能 对在本次对话中未读取的文件使用 update_file。
在任何 update_file 之前进行自检
- 我是否已收到系统提供的内容?
- 我是否确切了解当前状态?
- 我是否基于实际代码进行修改?
如果 任何 答案为 否 → 输出 read_file 操作,然后 停止。
这是一种用自然语言定义前置条件的尝试。它类似于:
def update_file(path, content):
assert path in conversation_state.read_files
# ... actual update
没有类型系统或自动运行时强制执行时,该规则 降低(但并未消除)LLM 在未先读取文件的情况下修改文件的概率。在测试中,我观察到 ~85‑90 % 的稳定性,但服务器端验证在关键情况下仍然至关重要。
多文件任务:提示再生成
我实现的最有效技术是 动态再生成提示,以强制 LLM 按多步骤计划执行。
具体场景
用户请求:“为项目添加身份验证。”
- 规划 – LLM 生成一个计划:
{
"plan": "I will modify these files in order:",
"files_to_modify": ["Auth.js", "Login.jsx", "App.jsx"]
}
-
第一个文件 – LLM 在
Auth.js上工作并完成它。 -
技巧 – 与其让 LLM “记住计划”,我 再生成提示,并在其中加入明确的 “下一步操作” 块:
### ⚠️ MULTI‑FILE TASK IN PROGRESS
You completed: Auth.js
Remaining files: Login.jsx, App.jsx
### 🚨 REQUIRED ACTION
Your next output MUST be:
{"action":"continue_multi_file","next_file":{"path":"Login.jsx"}}
Do NOT do anything else. Do NOT deviate from the plan.
LLM 不再需要 “记住” 任何内容。唯一可能的操作已经写进提示里。
为什么它很强大
- 状态(哪些文件已完成,哪些仍待处理)保存在 外部 JavaScript 中,而不在 LLM 的记忆里。
- 在每一步,我 再生成 包含更新后状态的提示。
- LLM 始终只看到 单一、明确的指令。
实现示例
// In the code
function buildPrompt(multiFileState) {
let prompt = BASE_PROMPT;
if (multiFileState) {
prompt += `
### TASK IN PROGRESS
Completed: ${multiFileState.completed.join(', ')}
Next: ${multiFileState.next}
Your ONLY valid action: continue_multi_file with ${multiFileState.next}
`;
}
return prompt;
}
这就是 状态注入:外部状态完全控制 LLM 接下来能做的事情。
混合内容的自定义分隔符
为了处理不同的“类型”(JSON、代码、元数据),DSL 使用 自定义分隔符:
#[json-data]
{"action":"create_file","file":{"path":"App.jsx"}}
#[end-json-data]
#[file-message]
This file implements the main app component.
#[end-file-message]
#[content-file]
export default function App() {
return Hello World;
}
#[end-content-file]
为什么不使用普通的 JSON 或 XML?
- 内容可能包含
{}、<>等,需要复杂的转义。 #[tag]…#[end-tag]语法唯一,使用正则即可轻松解析,并且与嵌入的语言无关。- 它表现为一种 上下文无关文法,用于分离语义层。
Source: …
错误示例指南
在 DSL 中直接嵌入“错误示例”可以教会模型常见的失败模式——语言的内联单元测试。
| ❌ 错误 | ✅ 正确 |
|---|---|
{"action":"start_multi_file","plan":{},"first_file":{...}} | {"action":"start_multi_file","plan":{},"first_file":{...}} |
#[json-data]{...}#[file-message]... | #[json-data]{...}#[end-json-data]#[file-message]... |
自然语言 DSL 的权衡
| 限制 | 后果 |
|---|---|
| ❌ 无可验证的类型 | 无静态类型检查 |
| ❌ 无自动语法验证 | 错误必须在运行时捕获 |
| ❌ 无用于转换的抽象语法树 (AST) | 无编译时优化 |
补偿措施
- ✅ 庞大的验证清单(8+ 项)
- ✅ 语义冗余 – 同一规则以多种方式表达
- ✅ 详尽的反模式文档
在使用自然语言构建 DSL 时,这些权衡是不可避免的,但最终的系统仍然可以是 健壮、透明 和 可控 的。
概览
The parser is a probabilistic LLM rather than a deterministic compiler.
If I were to evolve CodeForge in the future, a true mini‑DSL (JSON Schema + codegen) would reduce the prompt size by 30‑40 %. In the browser sandbox, however, the current choice is justified.
发送前检查清单
在 每一次 回复之前,进行验证:
| # | 检查项 | 失败时的修复 |
|---|---|---|
| 1 | JSON 有效 | 修正结构 |
| 2 | 标签完整 | 添加缺失的 #[end-*] 标签 |
单独使用时,这只能提供 40‑60 % 的可靠性。在我的系统中,它可以达到 80‑90 %,因为它在以下情况下充当 稳定性乘数:
- 模型已经 通道化(决策协议)
- 格式 刚性(自定义分隔符)
- 下一步操作是 确定性的(状态注入)
元验证 不是 主要功能——它是已经受限系统中的最终安全网。
Model Compatibility
✅ Works well with Claude 3.5, GPT‑4
❌ Smaller models will fail
❌ Less‑aligned models will ignore sections
我在暗示:该系统需要 “严肃”模型。
这是我接受的 架构约束 —— 就像说 “此库需要 Python 3.10+。”
上下文重新锚定
采用 “读‑写前检查” 规则:
- 出现在 决策协议(规划时)
- 出现在 可用操作(执行时)
- 出现在 发送前验证(验证时)
- 出现在 黄金法则(作为一般原则)
这是一种 战略性重复,而非随机冗余。它映射到安全关键系统:
- 相同的不变式
- 在多个层面进行验证
- 为每个上下文提供特定的措辞
模式示例
不良 vs. 良好模式
// BAD: Relying on the model's "memory"
"Remember that you have already read these files..."
// GOOD: Injecting explicit state
prompt += `Files already read: ${readFiles.join(', ')}`
// BAD: Giving open choices
"Decide which operation to perform"
// GOOD: Forcing the only legal move
"Your NEXT action MUST be: continue_multi_file"
输入 → 操作 → 下一个状态
| 输入模式 | 操作 | 下一个状态 |
|---|---|---|
"add X" | GATHER | EXECUTE |
"explain Y" | RESPOND | END |
而不是 “think what to do”,请使用 “if X then Y”。
嵌入任意内容
- 不要 使用 JSON – 转义噩梦
- 不要 使用 XML – 与 HTML/JSX 冲突
- 使用 唯一标签:
#[content]…#[end-content]
Pattern #5 – 冗余 = 覆盖, 而非噪声
- 重复关键规则
- 以不同的表述方式(semantic reinforcement)
- 在不同的上下文中(contextual re‑anchoring)
- 提供不同的理由(why, not just what)
在 5 000 个 token 并经过数月迭代后,最重要的教训是:
这个提示 并非“美观”。 它是 有效 的。
优化转变
| ❌ 不再寻找 | ✅ 开始优化 |
|---|---|
| 最短的提示 | 边缘情况的鲁棒性 |
| 最优雅的表述 | 故障模式覆盖 |
| 最通用的抽象 | 故障时的调试清晰度 |
结果:
- 冗余?是。
- 冗长?绝对。
- 有效?始终如一。
未来方向:我可能的去向
如果我要演进 CodeForge 2.0,我会探索一种 双代理 架构,而不是单一的 5 000 token 代理:
| 代理 | Token 预算 | 角色 |
|---|---|---|
| 规划代理 | 2 000 | 决定策略 |
| 执行代理 | 2 000 | 实施行动 |
好处
- 关注点分离
- 每个代理的上下文更少
- 可实现并行执行
有效的关键技术
| 评级 | 技术 |
|---|---|
| ⭐⭐⭐⭐⭐ | 状态注入 + 强制下一步操作 |
| ⭐⭐⭐⭐⭐ | 用于任务路由的决策表 |
| ⭐⭐⭐⭐⭐ | 用于结构化输出的自定义分隔符 |
| ⭐⭐⭐⭐⭐ | 不变式的上下文重新锚定 |
| ⭐⭐⭐⭐ | 作为安全网的元验证 |
| ⭐⭐⭐ | 视觉层次(有用但非关键) |
基本原则:
不要让 LLM “理解”——而是强迫它 “执行”。
将协议视为 DSL,而非对话。外部状态必须约束可能的操作,且验证必须始终在 服务器端或客户端 进行,绝不例外。冗余可以是 特性,而非缺陷。
行动号召
尝试 CodeForge:
该项目是 开源 — 完整的提示和验证系统实现可在仓库中获取。
社区提问
- 你是否曾经在自然语言中构建过嵌入式 DSL?
- 在你的提示中,“认知开销”的成本是多少?
- 双代理架构 vs. 单代理:有什么经验?
在评论中分享 — 这仍是大多未被探索的领域。