GitHub 上出现了一份《Jev 工程学》的完整中文翻译,保留了原文结构和七张插图。译者特别说明,自己与 TypeSafe 没有隶属关系,这是一份独立汇编。对国内开发者而言,它提供了一个少见的入口:不把 coding agent 只看成“会调用工具的聊天机器人”,而是把它当成一套需要重新设计的工程系统来理解。
这份设计笔记从一个很关键的问题展开:如果语言模型没有 KV cache,或者说每一步都不能依赖上一轮推理中已经存在的隐式上下文,那么 coding agent 应该如何持续工作?这个问题会把注意力从模型能力拉回到工程细节:状态放在哪里,任务如何恢复,工具结果如何压缩,失败如何重试,以及一个 agent 怎样在长时间工作后仍然保持可解释性。
Coding agent 的核心不是“记住”,而是“重建”
普通对话产品常常把上下文理解成一条不断增长的消息列表。模型每次请求都读取历史消息,再生成下一步回答。coding agent 面对的工作环境则复杂得多:它要读取代码、执行命令、观察测试结果、修改文件,并根据新的结果决定下一步动作。
如果每个循环都把全部历史原样塞回模型,系统很快会遇到三个问题:
- 上下文窗口被工具输出占满,真正重要的约束反而被淹没。
- 同样的历史被反复传输和解析,成本随任务长度上升。
- 一旦进程退出、请求失败或模型切换,agent 很难从一个明确的位置恢复。
因此,“没有 KV cache”并不只是一个模型层面的限制。它迫使工程师把原本隐藏在推理过程中的连续性显式化。agent 的记忆不能只存在于一次请求的上下文里,而应该拆成几类可重建状态:
- 任务状态:目标、约束、当前阶段和待办事项。
- 工作区状态:文件变更、测试结果、版本控制状态和环境信息。
- 交互事件:模型请求、工具调用、工具输出、错误和人工干预。
- 压缩摘要:对历史过程的稳定概括,而不是未经筛选的日志堆积。
这种设计的价值在于,下一次模型调用不需要“回忆整个过去”,只需要从状态存储中构造一份足以做出当前决策的上下文。
把 agent 看成事件循环
一个实用的 coding agent 通常可以抽象成下面的循环:
读取任务状态
↓
构造当前上下文
↓
请求模型选择下一步动作
↓
校验动作权限与参数
↓
执行工具
↓
记录结果并更新状态
↓
继续循环,或进入完成/失败状态
这里有一个容易被忽略的边界:模型只负责提出动作,系统负责决定动作是否可以执行。模型可以请求读取文件、运行测试或修改代码,但真正执行之前,运行时需要检查路径范围、命令权限、超时、输出大小和工作区状态。
这也是“为 coding agent 而作”与“给聊天机器人加几个函数调用”之间的差别。后者把工具调用当成一次请求的附属能力;前者则要为动作定义生命周期、可观测性和恢复语义。
一个最小的可恢复实现
下面的 Python 示例演示一种可以直接改造的最小结构。它不连接真实模型,而是用一个 decide 函数模拟模型决策。重点在于:每个动作和结果都写入 JSONL 事件日志,下一轮只根据日志重建状态。
运行前准备 Python 3.10 或更高版本。将代码保存为 agent_loop.py,直接执行 python agent_loop.py 即可。
from __future__ import annotations
import json
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
LOG = Path("agent-events.jsonl")
@dataclass
class State:
goal: str
phase: str = "working"
events: list[dict[str, Any]] = field(default_factory=list)
def append_event(event: dict[str, Any]) -> None:
with LOG.open("a", encoding="utf-8") as file:
file.write(json.dumps(event, ensure_ascii=False) + "\n")
def load_state(goal: str) -> State:
state = State(goal=goal)
if not LOG.exists():
return state
for line in LOG.read_text(encoding="utf-8").splitlines():
if line.strip():
state.events.append(json.loads(line))
return state
def run_command(command: list[str], timeout: int = 10) -> dict[str, Any]:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
return {
"command": command,
"returncode": result.returncode,
"stdout": result.stdout[-4000:],
"stderr": result.stderr[-4000:],
}
def decide(state: State) -> dict[str, Any]:
"""这里替换成真实模型调用,返回结构化动作。"""
tool_results = [
event for event in state.events if event.get("type") == "tool_result"
]
if not tool_results:
return {"tool": "shell", "args": {"command": ["python", "--version"]}}
return {"tool": "finish", "args": {"reason": "initial inspection completed"}}
def execute(action: dict[str, Any]) -> dict[str, Any]:
tool = action["tool"]
args = action.get("args", {})
if tool == "shell":
command = args["command"]
allowed = {"python", "pytest", "git"}
if not command or command[0] not in allowed:
raise ValueError(f"command is not allowed: {command}")
return run_command(command)
if tool == "finish":
return {"reason": args.get("reason", "done")}
raise ValueError(f"unknown tool: {tool}")
def main() -> None:
state = load_state("inspect the local Python environment")
action = decide(state)
append_event({"type": "action", "value": action})
if action["tool"] == "finish":
state.phase = "done"
append_event({"type": "status", "value": state.phase})
print("done")
return
try:
result = execute(action)
append_event({"type": "tool_result", "value": result})
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as error:
append_event({"type": "error", "value": str(error)})
raise
if __name__ == "__main__":
main()
这个示例还很小,但已经包含几个重要原则:
- 状态可重建:进程退出后,
agent-events.jsonl仍然保存了动作和结果。 - 动作结构化:模型输出的是工具名和参数,不是让系统盲目执行的一段自然语言。
- 权限集中校验:命令白名单位于执行层,而不是寄希望于 prompt 永远有效。
- 输出有上限:工具结果只保留最近一部分,避免单次输出拖垮后续上下文。
在生产环境中,可以把 JSONL 换成 SQLite、PostgreSQL 或专门的事件存储,并为每个任务增加 run_id、版本号、父事件和幂等键。
上下文工程:保留决策需要的信息
长任务的难点不是保存更多文本,而是保留正确的信息。一个失败的测试输出可能很长,但对下一步决策真正有用的内容通常只有:失败命令、退出码、关键错误位置、相关文件以及最近一次修改。
可以把上下文分成三层:
固定约束
包括用户目标、仓库规则、不能触碰的目录、测试命令、安全策略和输出格式。这些内容应该稳定地出现在每轮上下文中,不能因为摘要而丢失。
当前工作集
包括正在修改的文件、最近一次工具输出、当前失败原因和待验证假设。工作集应该短而新鲜,避免把整个仓库历史重复发送给模型。
可检索历史
包括较早的工具结果、已完成步骤和旧版本摘要。只有在当前决策需要时,才从中检索相关部分。
这个分层思路也解释了为什么“把所有消息都塞进 prompt”不是可靠的记忆方案。完整日志适合审计和调试,摘要适合推理,文件系统和版本控制适合保存实际产物。不同类型的信息应该由不同的存储承担。
工具调用必须有边界
Coding agent 能修改真实文件、安装依赖、运行命令,错误代价远高于普通问答。因此,工具层至少要处理以下问题:
- 路径边界:限制读写范围,拒绝通过
../或符号链接逃逸工作区。 - 命令权限:区分只读命令、构建命令和高风险命令。
- 资源限制:设置超时、输出大小、CPU 和内存限制。
- 幂等性:重复执行同一个动作时,不应无意中造成更多破坏。
- 结果记录:保留命令、参数、退出码和关键输出,便于重试与审计。
- 人工接管:涉及删除、发布、凭据或生产资源时,进入确认状态。
一个常见误区是只在系统提示词里写“不要执行危险命令”。提示词可以提供意图约束,但不能替代操作系统权限、容器隔离和执行器校验。真正的边界必须落在模型之外。
从 Jev 思路落地时的取舍
这套工程方向并不意味着所有 agent 都要立刻变成复杂的平台。小型脚本可以从三个动作开始:记录事件、限制工具、支持恢复。随着任务长度和风险增加,再逐步加入摘要、检查点、并发控制和人工审批。
可以采用下面的落地顺序:
- 给每次运行分配唯一的
run_id,把模型请求和工具结果写入持久化日志。 - 把工具执行从模型客户端中分离出来,统一做参数校验、超时和权限控制。
- 为任务定义明确状态,例如
working、waiting_approval、blocked、done和failed。 - 当上下文接近上限时生成摘要,同时保留原始事件,不要只留下不可追溯的压缩文本。
- 为重试设计幂等规则,区分“模型重新决策”和“工具重复执行”。
- 用真实仓库任务测试恢复能力,例如在测试中途杀掉进程,再从事件日志继续运行。
衡量一个 coding agent 是否工程化,不能只看它一次能否生成正确补丁,还要看它能否回答这些问题:它现在处于什么阶段?上一步执行了什么?为什么选择下一步?失败后能否从明确位置恢复?哪些动作需要人批准?
结语:把连续性做成系统能力
《Jev 工程学》的价值,至少可以从这个入口理解:当模型不再拥有可靠的隐式连续上下文时,coding agent 的连续性必须由系统提供。事件日志、结构化动作、可控工具、摘要和检查点,组成了一条比“把聊天记录越堆越长”更稳的路径。
对开发者来说,最值得带回项目的不是某个固定框架,而是一种设计检查表:
- 状态是否能够在进程重启后重建?
- 模型输出是否经过结构化解析和权限校验?
- 工具结果是否有大小、时间和范围限制?
- 长任务是否有摘要与检查点?
- 失败是否可解释、可重试、可人工接管?
当这些问题都有明确答案时,coding agent 才不只是一个会调用 shell 的模型,而是一套能够长期运行、恢复和审计的工程系统。