Agency 与 Harness — 智能从哪里来
Agency(感知、推理、行动的能力)来自模型训练,不是来自代码编排。Harness 是让智能落地的载具。
一个被误解的概念
当人们说「我在开发 AI Agent」时,大多数人在做的是 Harness 工程——为模型构建一个可以工作的环境,而不是在训练模型本身。
Agency——感知、推理、行动的能力——是训练出来的,不是编出来的。你不能通过堆叠 if-else 分支来编码出智能。
历史已经写好了铁证
每一个里程碑都指向同一个事实:Agency 是训练出来的。
2013 — DeepMind DQN
一个神经网络,只看原始像素和分数,学会玩 49 款 Atari 游戏,达到职业人类水平。没有规则,没有决策树。
2019 — OpenAI Five
5 个网络自我对弈 45,000 年的 Dota 2,2-0 击败世界冠军 OG。没有脚本策略,完全从经验中学会团队协作。
2019 — AlphaStar
星际争霸 II 中 10-1 击败职业选手,达到宗师段位(前 0.15%)。信息不完全、实时决策、组合爆炸。
2019 — 腾讯绝悟
王者荣耀 5v5 击败 KPL 职业选手。训练一天 = 人类 440 年。没有英雄克制表,从零学会整个游戏。
2024-25 — LLM Agent
Claude、GPT、Gemini——在人类全部代码和推理上训练的大语言模型——被部署为编程 Agent。架构与之前每一个 Agent 完全相同。
Agent 不是什么
# 提示词水管工式 "Agent"
if user_input == "写代码":
result = llm("请帮我写代码")
elif user_input == "调试":
result = llm("请帮我调试")
else:
result = llm("我不知道你在说什么")
# 这是一个 if-else 外壳包着 LLM
# 不是 Agent,是有妄想的 shell 脚本
这叫「提示词水管工」——用 if-else 分支把 LLM 包起来。
模型做的唯一的事就是填空补全文本。
代码替模型做了所有决策:什么时候调、调什么。
这等于你替员工干了活,然后说是「团队协作」。
真正的 Agent 是模型自己决定什么时候用什么工具。
代码只负责执行模型的要求,不替模型思考。
这个区别就是「Agent」和「脚本」的分界线。
鲁布·戈德堡机械:过度工程化的、脆弱的过程式流水线。
拖拽式工作流构建器、无代码「AI Agent」平台、提示词链编排库——它们做出来的是鲁布·戈德堡机械,不是 Agent。那是 GOFAI(经典符号 AI)的现代还魂,几十年前就被学界抛弃了。
Harness 工程师的五件实事
如果你在读这个课程,你很可能是一名 Harness 工程师。以下是你真正的工作:
给 Agent 双手——文件读写、Shell 执行、API 调用。每个工具是 Agent 在环境中可以采取的一个行动。
给 Agent 领域专长——产品文档、架构记录、风格指南。按需加载,不前置塞入。
给 Agent 干净的记忆——子 Agent 隔离防止噪声,上下文压缩防止溢出,任务系统让目标持久化。
给 Agent 边界——沙箱化文件访问,破坏性操作要求审批,实施信任边界。
Agent 在你的 Harness 中执行的每一条行动序列,都是训练下一代模型的信号。你的 Harness 不仅服务于 Agent,还可以帮助进化 Agent。
Claude Code 的完整架构
Claude Code 是我们所见过的最优雅的 Agent Harness 实现。点击任意组件查看其作用:
核心循环
上下文管理
持久化与协作
检验你的理解
你的团队用 if-else 分支编排 LLM API 调用,把它叫做「AI Agent」。这种做法的核心问题是什么?
作为一名 Harness 工程师,你最重要的工作是什么?
Agent 循环 — 一切从 while 开始
一个工具 + 一个循环 = 一个 Agent。整个 AI 编程 Agent 的秘密就在一个 pattern 里。
最小循环:用户 → LLM → 工具 → 循环
点击「下一步」看数据如何在 Agent 循环中流动:
s01 — 完整的 Agent Loop
这是 s01 的核心代码——整个 Agent 的秘密就在这几行:
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM,
messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append(
{"role":"assistant",
"content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(
block.input["command"])
results.append({
"type":"tool_result",
"tool_use_id": block.id,
"content": output})
messages.append(
{"role":"user",
"content": results})
定义 agent_loop 函数,接收消息历史。
开始无限循环——模型会在合适的时候自己停下来。
调用 LLM:传入模型名、系统提示、消息历史、工具定义。
告诉模型可用哪些工具,限制最大输出 token。
把模型的回复追加到消息历史。
如果模型没有请求调工具(stop_reason 不是 tool_use)……
直接返回——模型觉得任务完成了。
否则,准备收集工具执行结果。
遍历模型回复中的每一个内容块……
如果是工具调用请求……
提取命令,执行 bash。
把结果包装成 tool_result 格式……
附带工具调用 ID,这样模型知道哪个结果对应哪个请求。
把所有工具结果作为新的「用户」消息追加。
循环回到开头,模型现在能看到工具结果了。
这个循环永远不变。后续 11 个课程都在这个循环之上叠加 Harness 机制,但循环本身始终不变。循环属于 Agent,机制属于 Harness。
s01 唯一的工具:bash
第一个课程只有一个工具——bash。这就是够了:
TOOLS = [{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type":"string"}
},
"required": ["command"],
},
}]
工具列表——目前只有一个。
工具名叫「bash」。
告诉模型这个工具的用途:运行 Shell 命令。
定义工具的输入格式。
输入是一个对象(字典)。
包含一个属性。
command 是字符串类型。
定义必填字段。
command 是必填的——必须告诉 bash 跑什么。
这就是一个完整的工具定义。
找 Bug 挑战
下面这段代码有一个关键错误——模型永远不会停止。找出问题所在的那一行:
点击有 Bug 的那一行:
def agent_loop(messages):
while True:
response = client.messages.create(...)
messages.append(response.content)
results = run_tools(response)
messages.append(results)
检验你的理解
在 Agent Loop 中,谁决定循环是否继续?
当模型调用工具后,为什么 tool_result 必须包含 tool_use_id?
工具系统与子代理 — 循环不变,能力翻倍
加一个工具 = 加一个 handler,循环不用动。大任务拆小,每个子任务干净的上下文。
s02 — Dispatch Map:名字 → 处理函数
s01 的循环一行没改。只是把工具数组变长了,加了一个分发字典:
bash
运行 Shell 命令——安装包、运行测试、查看文件。
read_file
读取文件内容——Agent 的「眼睛」,可以看代码、配置、日志。
write_file
写入文件内容——创建新文件、生成代码、保存结果。
edit_file
精确替换文件中的文本——修改代码、修复 Bug、重构。
关键代码:分发字典
TOOL_HANDLERS = {
"bash":
lambda **kw: run_bash(kw["command"]),
"read_file":
lambda **kw: run_read(kw["path"]),
"write_file":
lambda **kw: run_write(
kw["path"], kw["content"]),
"edit_file":
lambda **kw: run_edit(
kw["path"],kw["old_text"],
kw["new_text"]),
}
# 循环中的调用方式:
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
分发字典——工具名字映射到处理函数。
「bash」工具 → 调用 run_bash 函数,传入 command。
「read_file」→ 调用 run_read,传入 path。
「write_file」→ 调用 run_write,传入路径和内容。
「edit_file」→ 调用 run_edit,传入路径、旧文本和新文本。
结束字典定义。
在循环中,根据模型请求的工具名查找处理函数。
用模型的输入参数调用处理函数——就这么简单。
新增工具只需要两步:(1) 写处理函数,(2) 注册进字典。循环代码永远不需要修改。这就是「开闭原则」——对扩展开放,对修改关闭。
s04 — 子代理:独立上下文,干净隔离
想象一个侦探(主 Agent)派助手(子 Agent)去调查线索。助手去现场,收集证据,回来只说结论。侦探的记忆不会被现场细节污染。
子代理的核心实现
def run_subagent(prompt: str) -> str:
sub_messages = [
{"role":"user",
"content": prompt}]
for _ in range(30):
response = client.messages.create(
model=MODEL, system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=CHILD_TOOLS, max_tokens=8000)
sub_messages.append(...)
if response.stop_reason != "tool_use":
break
# ... execute tools, append results
return "".join(
b.text for b in response.content
if hasattr(b, "text"))
定义 run_subagent——接收任务描述,返回摘要。
创建全新的空白消息历史——这是隔离的关键!
只有一条用户消息:任务描述。
最多循环 30 次(安全限制)。
用独立的系统提示和子工具列表调用 LLM。
子 Agent 看不到主对话历史。
子 Agent 没有递归创建子 Agent 的能力。
如果任务完成了,退出循环。
执行工具、追加结果(与主循环相同)。
只返回最终文本摘要给父 Agent。
子上下文被销毁——节省了宝贵的 token 空间。
匹配:概念 → 角色
把每个概念拖到它对应的描述上:
工具名字 → 处理函数的字典映射
独立 messages[] 执行子任务,只返回摘要
告诉模型工具的名称、描述和输入格式
检验你的理解
子 Agent(s04)和直接在主循环中执行有什么关键区别?
你想给 Agent 加一个新工具(比如 search_web)。需要修改循环代码吗?
任务系统与团队协作 — 目标超越对话
大目标拆成小任务排好序记在磁盘上。任务太大一个人干不完要能分给队友。队友之间要有统一的沟通规矩。
s07 — 上下文会丢,磁盘不会
想象你在写一部长篇小说。每章都记录在笔记本上,即使失忆了也能翻回来。Agent 也一样——上下文窗口有限,但磁盘上的文件不会消失。
把大目标拆成小任务,每个任务写入 .tasks/task_N.json 文件
任务 B blockedBy 任务 A——A 不完成,B 不能开始
开始任务标记 in_progress,完成后自动解除下游依赖
上下文压缩后,任务状态依然在磁盘上——不会丢失
TaskManager:CRUD + 依赖图
class TaskManager:
def create(self, subject, description=""):
task = {
"id": self._next_id,
"subject": subject,
"status": "pending",
"blockedBy": [],
}
self._save(task)
def _clear_dependency(self, completed_id):
"""完成后自动解除下游依赖"""
for f in self.dir.glob("task_*.json"):
task = json.loads(f.read_text())
if completed_id in task["blockedBy"]:
task["blockedBy"].remove(
completed_id)
self._save(task)
TaskManager 类管理任务的增删改查。
创建任务:主题、描述、初始状态 pending。
每个任务有唯一 ID(自增)。
主题是任务的一句话描述。
初始状态:待处理。
依赖列表——哪些任务要先完成。
保存到磁盘(JSON 文件)。
关键方法:完成后自动解除依赖。
遍历所有任务文件。
读取每个任务。
如果已完成任务的 ID 在 blockedBy 列表中……
移除这个依赖。
保存更新后的任务到磁盘。
「大目标要拆成小任务,排好序,记在磁盘上。」——状态在对话之外存活,因为对话会压缩,磁盘不会。
s09 — Agent 团队:多线程 + JSONL 邮箱
子 Agent 是一次性的——派出去、干完活、销毁。队友是持久的——有名字、有角色、可以反复被召唤。
通信机制:JSONL 邮箱
队友之间通过文件通信——像邮局一样,每个队友有自己的信箱文件:
class MessageBus:
def send(self, sender, to, content,
msg_type="message"):
msg = {"type": msg_type,
"from": sender,
"content": content,
"timestamp": time.time()}
path = self.dir / f"{to}.jsonl"
with open(path, "a") as f:
f.write(json.dumps(msg) + "\n")
def read_inbox(self, name):
path = self.dir / f"{name}.jsonl"
msgs = [json.loads(l)
for l in path.read_text()
.strip().splitlines()
if l]
path.write_text("") # drain
return msgs
MessageBus——管理所有队友之间的通信。
发送消息:指定发送者、收件人、内容和类型。
构建消息对象:类型、来源、内容、时间戳。
找到收件人的邮箱文件(alice.jsonl)。
以追加模式打开文件(append-only)。
写入一行 JSON + 换行符。
读取邮箱:找到自己的邮箱文件。
逐行解析 JSON 得到消息列表。
过滤空行。
关键操作:读取后清空文件(drain/排干)。
返回所有消息。
5 种消息类型
message
普通文本消息——队友之间的日常沟通
broadcast
广播——发给所有队友的公共通知
shutdown_request
关闭请求——请求队友优雅退出(s10)
shutdown_response
关闭响应——同意或拒绝关闭请求
plan_approval
计划审批——队友审批或拒绝执行计划
检验你的理解
你的 Agent 正在重构一个大型代码库。任务被拆分为:A(数据库迁移)、B(API 修改,依赖 A)、C(前端适配,依赖 B)。上下文窗口快满了。
上下文窗口快满时,之前创建的任务状态会丢失吗?
JSONL 邮箱通信方式的核心优势是什么?
隔离与并行执行 — 各干各的,互不干扰
任务管目标,Worktree 管目录,按 ID 绑定。并行执行时,每个任务在独立的 Git Worktree 中工作。
问题:两个 Agent 同时改同一个文件
想象两个厨师同时在同一个灶台上做菜——一个在煎牛排,另一个在煮汤。锅就那么大,迟早要撞车。
s12 — Git Worktree = 独立厨房
Git Worktree 让同一个仓库拥有多个独立的工作目录:
「各干各的目录,互不干扰。」——任务管目标(控制面),Worktree 管目录(执行面),按 Task ID 绑定。
WorktreeManager 核心操作
def create(self, name, task_id=None,
base_ref="HEAD"):
path = self.dir / name
branch = f"wt/{name}"
self._run_git(
["worktree", "add",
"-b", branch,
str(path), base_ref])
entry = {"name": name,
"path": str(path),
"branch": branch,
"task_id": task_id,
"status": "active"}
# 保存到 index.json
idx = self._load_index()
idx["worktrees"].append(entry)
# 绑定任务
if task_id:
self.tasks.bind_worktree(
task_id, name)
创建 Worktree:指定名称、关联任务、基准引用。
计算路径:.worktrees/{name}。
自动生成分支名:wt/{name}。
执行 Git 命令创建 Worktree。
参数:worktree add,创建新分支 -b。
指定路径和基准点(HEAD = 当前提交)。
构建索引条目。
名称、路径、分支名。
关联的任务 ID。
状态:活跃。
保存到 .worktrees/index.json。
如果有关联任务……
在任务的 JSON 中记录 Worktree 名称——绑定完成。
EventBus:观察生命周期的窗口
s12 还引入了一个最小的事件总线——EventBus——记录所有 Worktree 和任务的生命周期事件:
worktree.create.before
Worktree 创建前
worktree.create.after
Worktree 创建成功
worktree.create.failed
Worktree 创建失败
worktree.remove.before
Worktree 移除前
task.completed
关联任务被标记完成
worktree.keep
Worktree 被保留(不删除)
事件只做「追加写入」(append-only),永不修改。这是可观测性的基础——你可以看到发生了什么,但不能篡改历史。
12 步递进:从 0 到自治执行
每一步只加一个 Harness 机制,循环永远不变:
阶段一:循环
s01 Agent Loop + s02 Tool Use —— while + stop_reason + dispatch map
阶段二:规划与知识
s03 计划 + s04 子代理 + s05 技能加载 + s06 上下文压缩
阶段三:持久化
s07 任务系统(JSON 文件 + 依赖图)+ s08 后台任务(守护线程)
阶段四:团队
s09 团队 + s10 协议 + s11 自治 + s12 Worktree 隔离
最终测验
你的 Agent 团队有 3 个成员同时在 3 个不同的任务上工作,都涉及修改 src/api.py。你用 s12 的 Worktree 隔离方案。