01

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 不是什么

MISCONCEPTION

# 提示词水管工式 "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 实现。点击任意组件查看其作用:

核心循环

🔃
Agent Loop
🔧
Tools

上下文管理

📚
Skills
💾
Context Compact
👥
Subagent

持久化与协作

📋
Task System
🤝
Agent Teams
🔀
Worktree
点击任意组件查看其作用

检验你的理解

你的团队用 if-else 分支编排 LLM API 调用,把它叫做「AI Agent」。这种做法的核心问题是什么?

作为一名 Harness 工程师,你最重要的工作是什么?

02

Agent 循环 — 一切从 while 开始

一个工具 + 一个循环 = 一个 Agent。整个 AI 编程 Agent 的秘密就在一个 pattern 里。

最小循环:用户 → LLM → 工具 → 循环

点击「下一步」看数据如何在 Agent 循环中流动:

👤
用户
🧠
LLM
🔧
工具
💬
输出
点击「下一步」开始动画

s01 — 完整的 Agent Loop

这是 s01 的核心代码——整个 Agent 的秘密就在这几行:

CODE — s01_agent_loop.py

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。这就是够了:

CODE

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 的那一行:

1 def agent_loop(messages):
2 while True:
3 response = client.messages.create(...)
4 messages.append(response.content)
5 results = run_tools(response)
6 messages.append(results)

检验你的理解

在 Agent Loop 中,谁决定循环是否继续?

当模型调用工具后,为什么 tool_result 必须包含 tool_use_id?

03

工具系统与子代理 — 循环不变,能力翻倍

加一个工具 = 加一个 handler,循环不用动。大任务拆小,每个子任务干净的上下文。

s02 — Dispatch Map:名字 → 处理函数

s01 的循环一行没改。只是把工具数组变长了,加了一个分发字典:

🐛

bash

运行 Shell 命令——安装包、运行测试、查看文件。

📄

read_file

读取文件内容——Agent 的「眼睛」,可以看代码、配置、日志。

✏️

write_file

写入文件内容——创建新文件、生成代码、保存结果。

✂️

edit_file

精确替换文件中的文本——修改代码、修复 Bug、重构。

关键代码:分发字典

CODE — s02_tool_use.py

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)去调查线索。助手去现场,收集证据,回来只说结论。侦探的记忆不会被现场细节污染。

子代理的核心实现

CODE — s04_subagent.py

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 空间。

匹配:概念 → 角色

把每个概念拖到它对应的描述上:

Dispatch Map
Subagent
Tool Schema

工具名字 → 处理函数的字典映射

拖到这里

独立 messages[] 执行子任务,只返回摘要

拖到这里

告诉模型工具的名称、描述和输入格式

拖到这里

检验你的理解

子 Agent(s04)和直接在主循环中执行有什么关键区别?

你想给 Agent 加一个新工具(比如 search_web)。需要修改循环代码吗?

04

任务系统与团队协作 — 目标超越对话

大目标拆成小任务排好序记在磁盘上。任务太大一个人干不完要能分给队友。队友之间要有统一的沟通规矩。

s07 — 上下文会丢,磁盘不会

想象你在写一部长篇小说。每章都记录在笔记本上,即使失忆了也能翻回来。Agent 也一样——上下文窗口有限,但磁盘上的文件不会消失。

1
创建任务

把大目标拆成小任务,每个任务写入 .tasks/task_N.json 文件

2
设定依赖

任务 B blockedBy 任务 A——A 不完成,B 不能开始

3
执行与更新

开始任务标记 in_progress,完成后自动解除下游依赖

4
持久化存活

上下文压缩后,任务状态依然在磁盘上——不会丢失

TaskManager:CRUD + 依赖图

CODE — s07_task_system.py

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 是一次性的——派出去、干完活、销毁。队友是持久的——有名字、有角色、可以反复被召唤。

👑
Lead 队长
🧑‍💻
Alice 编码
🧪
Bob 测试
点击「下一步」看团队协作流程

通信机制:JSONL 邮箱

队友之间通过文件通信——像邮局一样,每个队友有自己的信箱文件:

CODE — s09 MessageBus

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 邮箱通信方式的核心优势是什么?

05

隔离与并行执行 — 各干各的,互不干扰

任务管目标,Worktree 管目录,按 ID 绑定。并行执行时,每个任务在独立的 Git Worktree 中工作。

问题:两个 Agent 同时改同一个文件

想象两个厨师同时在同一个灶台上做菜——一个在煎牛排,另一个在煮汤。锅就那么大,迟早要撞车。

s12 — Git Worktree = 独立厨房

Git Worktree 让同一个仓库拥有多个独立的工作目录:

项目仓库/ 主工作目录(HEAD)
auth.py 原始版本
.worktrees/ 隔离工作目录
auth-refactor/ Alice 的独立目录(分支 wt/auth-refactor)
auth-tests/ Bob 的独立目录(分支 wt/auth-tests)
.tasks/ 任务看板(控制面)
task_1.json {"id":1, "subject":"重构认证", "worktree":"auth-refactor"}
task_2.json {"id":2, "subject":"添加测试", "worktree":"auth-tests"}
💡
格言

「各干各的目录,互不干扰。」——任务管目标(控制面),Worktree 管目录(执行面),按 Task ID 绑定。

WorktreeManager 核心操作

CODE — s12 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 隔离方案。

Worktree 隔离的关键在于文件系统层面的独立。三个 Agent 操作的是三个不同的物理目录。">

三个 Agent 同时修改 src/api.py 时,为什么不会冲突?

这涉及到两个层面的分离——谁管目标,谁管执行空间?">

Task 和 Worktree 的关系是什么?

学完这 12 步递进,最重要的认识是什么?