🏛 给 AI 配一间办公室:Harness Engineering 六大模块与它的实现

写在前面:今天这节课的主题叫 Harness Engineering(智能体工程) ,readme 开篇一句话给出了定义------"用工程化手段,让 AI Agent 可靠、持续完成任务。" 这句话里有两个关键词:可靠持续。会调 API 的人很多,但能让 AI 稳定地干完一件长活的人不多。上几节课学的 Memory、LangGraph、结构化输出、SSE------这些其实都是 Harness 的零件。今天这节是总纲:把这些零件装进同一张图纸。readme 还给出了对应的岗位画像------"AI 应用开发工程师(Agentic RAG 应用)、AI Agent 开发工程师(Harness Agent FDE)"。以下所有代码和概念均来自课堂真实文件。


一、LLM 是个没入职的应届生

先想一个问题------LLM 那么聪明,为什么不能直接拿来干活?

因为它是个**"裸脑"**:会说话、会推理,但:

缺什么 后果
没有手 不能读写文件、不能跑命令
没有记忆 关掉即失忆(前面讲过的"金鱼")
没有边界 要是真给了它命令执行权,它能 rm -rf /
没有履历 出了问题你不知道它刚才做了什么

Harness 就是给这颗"裸脑"配的一整套办公设施------工位、工具箱、资料柜、门禁、笔记本、监控。装完之后,它才从"聊天机器人"变成"Agent"。

readme 里那句被反复提到的公式,今天终于要完整展开了:

ini 复制代码
Agent = LLM(大脑)+ Harness(tool + mcp + rag + skill ... + ...)

二、六大基础模块:一间办公室的六件标配

readme 列了六个模块,说得很清楚:

"包含 6 个基础模块。"

模块 职责 办公室类比
Loop 主循环 核心控制层,调用大模型、分发工具调用、判断终止条件 工作节奏
Tool 工具集 LLM 能使用的工具集 工具箱
Context 上下文管理器 输入给模型的全部内容 + 压缩 + 窗口控制 桌上的资料
Environment 沙箱环境 工具运行时的隔离环境 工位与门禁
Memory 记忆层 跨轮次、跨会话状态持久化 笔记本
Observability 可观察性 日志、Trace、评估指标、调试、回放、评测 监控与复盘

逐个看。

1. Loop 主循环:心跳

readme 原文:

"Loop 主循环------自主长时间干活。ReAct。核心控制层,调用大模型,分发工具调用,判断任务终止条件。Agent 的主执行流。"

这是整个 Agent 的心脏。它干的事很简单,但必须永不停歇:

markdown 复制代码
while (任务未完成) {
    1. 把当前上下文发给 LLM
    2. LLM 决定:直接回答?还是调用工具?
    3. 如果调工具 → 执行 → 把结果塞回上下文 → 回到第 1 步
    4. 如果直接回答 → 结束
}

这就是 ReAct(Reason + Act)------推理一步、行动一步、观察结果、再推理。前面讲 Memory 时提到过这个流程,今天它是六大模块里的第一号。

关键在"判断终止条件"------LLM 可能陷入死循环(反复调同一个工具),也可能话说到一半就停。主循环必须管住这两件事。

2. Tool 工具集:给它一双手

"Tool 工具集,llm 能使用的工具集。"

工具的本质是把 LLM 的"文字能力"变成"行动能力"。前面学的结构化输出、Tool Call、JSON Schema 全部用在这里------每个工具都需要:

  • 名字(bashread_file
  • 描述(LLM 靠它判断什么时候用)
  • 参数 Schema(LLM 靠它生成合法参数)

agent.py 里有 4 个基础工具(下面细讲)------bash、read_file、write_file、edit_file。就这四个,已经能覆盖"写代码"这件事的绝大部分场景了。

3. Context 上下文管理器:资料怎么摆

"Context 上下文管理器。无状态。输入给模型的全部内容:系统提示词、历史对话、rag、检索、es 上下文。工具返回结果。上下文压缩、窗口控制。"

这里有个反直觉的点------readme 特意标了**"无状态"**。

LLM 本身是无状态的(每次调用都不记得上次),所以"上下文管理器"的职责就是:每次调用前,把该给的资料全都准备好,摆到它面前。

包括:

内容 来源
系统提示词 代码里写死的 SYSTEM
历史对话 messages 数组 / Memory
RAG 检索结果 向量数据库(前面学的 Milvus)
工具返回结果 上一轮工具的执行输出

而且还要管**"窗口控制"和"上下文压缩"**------前面学的截断、总结、检索,就是这一模块的具体手段。工具返回了 50000 字符的结果?得压缩。历史对话太长了?得截断。

4. Environment 沙箱环境:工位与门禁

"Environment 沙箱环境。工具运行时的隔离环境,文件系统、网络、权限、工作区隔离。"

这条是安全底线。你不能让 AI 在整台电脑上随便跑命令------它得被关在一个"工位"里:

  • 文件系统:只能读写指定目录
  • 网络:能访问哪些地址
  • 权限:能执行哪些命令
  • 工作区隔离:一个任务的改动不能污染另一个任务

agent.py 里的 safe_path() 就是这一模块的落地------所有文件操作都要先过这道安检

5. Memory 记忆层:笔记本

"Memory 记忆层。跨轮次、跨会话状态持久化状态,短期记忆 + 长期记忆。"

这一模块前面上了整整两节课(金鱼记忆上/下篇)------短期用内存/文件,长期用向量数据库。今天不重复,只强调它的定位:Memory 是六大模块之一,不是可选项。

6. Observability 可观察性:监控与复盘

"Observability 可观察性。日志(分析)、Trace、评估指标、调试、回放、评测。"

这是最容易被忽视、但在生产环境最救命的一环。

想象一下:Agent 跑了 20 步,最后给了一个错误答案。你怎么办?

  • 没有 Observability:一脸茫然,不知道哪一步错了
  • 有 Observability:看 Trace,第 7 步读错了文件 → 第 12 步基于错误信息推理 → 定位到根因

readme 列了五个能力:日志、Trace、评估指标、调试、回放、评测。其中"回放"最有意思------把整个执行过程重放一遍,像看录像一样复盘。


三、高级模块:从单人到团队

readme 说得很清楚:

"6 大基础模块是最小内核;sub-Agents 属于高级编排的扩展模块,不是底层必选,但现代 Agent Harness 工程体系普遍把它作为标准模块。"

除了六大内核,还有几个可插拔的高级模块:

模块 作用
Sub Agents 子智能体,任务拆分与并行
hooks 生命周期钩子
Policy & Safety 权限、输出过滤、资源配额

Sub Agents:外包团队

"职责清晰,拆分。上下文互不干扰。主 Agent 分配任务给子 Agent,子 Agent 像子进程,上下文不被打扰。主 Agent 也不会因为子 Agent 的运行,上下文受拖累。"

"上下文互不干扰"------这七个字是 Sub Agent 存在的根本理由。

举个具体例子,readme 里那个"多 Agent 分工":

css 复制代码
Agent A 写前端代码
Agent B 写后端代码
Agent C 写测试
Agent D 部署

如果全让一个 Agent 干------它的上下文里会堆满前端代码、后端代码、测试代码、部署脚本,全都搅在一起。而拆成四个子 Agent:

  • A 的上下文里只有前端相关
  • B 的上下文里只有后端相关
  • 主 Agent 只需要知道"A 完成了、B 完成了"

readme 还点出了"按需加载工具"这一层:

"派发任务给子 Agent,独立的运行上下文,按需加载 tools,不用一次性加载那么多。"

这也呼应了前面 LangGraph 那节课讲的------单 Agent 把所有工具描述都塞进 system prompt,token 贵还干扰思考。 拆成子 Agent 后,每个子 Agent 只带自己需要的工具。


四、为什么是 Agent,而不是 Workflow?

这是 readme 里最有含金量的一段,值得单独开一节。

"固定的 workflow 是预先写死的步骤,(coze/dify/n8n 23-25 年,较固定、简单的任务),只能按预设路径执行,无法应对不确定、动态变化的任务。多 Agent(含 subAgent)可自主拆解任务,按需调用能力,根据中间结果调整执行分支,适合需求模糊、存在未知问题的复杂场景,具备更强的自适应与容错能力。"

Workflow 是一台自动售货机------投币、选号、出货。路径写死了,遇到问题不会变通。

Agent 是一个员工------你交给他一个模糊的任务,他自己决定先做什么、用什么工具、遇到问题怎么绕。

readme 给了一个特别好的对比案例:做一份行业技术调研报告。

简单版本:Workflow 就够了

复制代码
问题 → 行业关键字 → 技术关键字 → 上网搜 → 分析 → 生成报告

六个固定步骤,一气呵成。这种任务路径明确、没有意外,Workflow 又稳又便宜。

专业版本:必须上多 Agent

"任务是专业的。分析需要哪些 agent?"

readme 列出的团队配置:

角色 职责
主 Agent 负责任务拆解与整体调度
子 Agent(检索) 检索资料、反爬、判断资料优劣、自动分析、切换数据源
摘要 Agent 压缩提取
校验 Agent "不要信资料,网上要有些分辨的"
情报 Agent 找到行业内大佬的联系方式
报告 Agent 生成最终报告

注意那个校验 Agent 的说明------readme 原话:

"不要信资料,网上要有些分辨的。"

这句话道破了多 Agent 的真正价值------不是并行提速,而是互相制衡。 检索 Agent 找来的资料,要经过校验 Agent 质疑真伪。跟前面 LangGraph 那节课提到的"AutoGen 法庭"是同一个思路。

而 Workflow 做不到这一点------它的第六步永远是"生成报告",不会因为第五步发现资料可疑就临时插入一个"再查一遍"的分支。

动态调整执行分支,这才是 Agent 相对 Workflow 的质变。


五、多 Agent 的业务隔离:三件套

readme 末尾列了多 Agent 隔离的三个手段:

"专属 SYSTEM prompt(多个 system,主 system 规划、分工,子 system 执行任务);子进程;独立的上下文(子 Agent 返回结果,有全新的上下文,不会干扰主 Agent 上下文)。"

隔离手段 隔离了什么
专属 SYSTEM prompt 角色定位------主 Agent 规划、子 Agent 执行
子进程 进程空间
独立上下文 对话历史

这三条在 agent.py 里全部有对应实现,我们马上进代码。


六、338 行代码:把六大模块装进一间办公室

现在进入今天的重头戏------agent.py。这份代码不长(338 行),但六大模块一个不少。我们按模块拆。

配置层:办公室的门牌号

python 复制代码
import os
import re
import subprocess
from pathlib import Path
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv(override=True)
# Agent 工作目录 安全的,被授权的
WORKDIR = Path.cwd()
# 从 .env 读取模型名,全脚本统一用这一个常量
MODEL = os.getenv("DEEPSEEK_MODEL")

client = OpenAI(
    base_url=os.getenv("DEEPSEEK_BASE_URL"),
    api_key=os.getenv("DEEPSEEK_API_KEY"),
)

代码注释里有句话很关键:

"Agent 工作目录,安全的,被授权的。"

WORKDIR = Path.cwd() ------ 这不是随便拿个路径,这是划定"办公室"的范围。后面所有文件操作都不能迈出这个门。

还有个小知识点,代码注释特意提了:

"python 没有常量变量之分,都是变量,用约定大写来表达"

Python 没有 const,全靠全大写命名约定表示"这是常量,别改"。

Environment 模块:门禁系统

python 复制代码
def safe_path(p: str) -> Path:
    # pathlib.Path 特有的 / 运算符,不是除法,运算符的重载
    # 相当于路径的拼接
    path = (WORKDIR / p).resolve()
    # 逻辑判断语法
    if not path.is_relative_to(WORKDIR):
        # 抛出异常
        raise ValueError(f"Path escapes workspace:{p}")
    return path

十行代码,干掉了整个"逃出工作目录"的攻击面。两个细节值得说:

第一,WORKDIR / p 是运算符重载。 注释专门解释了:

"pathlib.Path 特有的 / 运算符,不是除法,运算符的重载。"

Python 里 / 本来是除法,但 Path 对象重载了它------变成路径拼接。所以 WORKDIR / "src/app.py" 得到 /Users/xx/project/src/app.py

第二,.resolve() 之后再校验。 为什么要 resolve?因为 ../../etc/passwd 这种路径,不 resolve 你根本看不出它跑哪去了。resolve 把 .. 全部展开成绝对路径,然后才判断 is_relative_to(WORKDIR)

如果 LLM 试图访问 ../../../etc/passwd(经典路径穿越攻击),第一次校验就会拦住------抛 ValueError 而不是"默默允许"。

这就是 readme 说的"工作区隔离"。

Policy & Safety 模块:危险命令黑名单

python 复制代码
def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", ">/dev/"]
    # any() 就是只有一个满足就返回真
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked."

    try:
        r = subprocess.run(command, shell=True, cwd=WORKDIR,
            capture_output=True, text=True, errors="replace", timeout=120,
        )
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout(120s)"
    except (FileNotFoundError, OSError) as e:
        return f"Error: {e}"

这是"Policy & Safety"模块的极简落地版------一份危险命令黑名单

危险命令 拦它的原因
rm -rf / 删库跑路一条龙
sudo 提权,超出授权范围
shutdown / reboot 把服务器关了
>/dev/ 直接写设备文件

注意这里的黑名单策略是"字符串包含判断"d in command)------非常朴素,甚至可以说很脆弱(稍微变形就能绕过)。但它体现了这个模块的定位:Policy & Safety 不是随便写写,是需要认真设计的一层。

三个工程细节也在这里:

细节 代码 作用
超时保护 timeout=120 命令卡死不至于拖死 Agent
输出合并 r.stdout + r.stderr 错误信息也是信息,LLM 需要看到
长度截断 out[:50000] 防止一次输出撑爆上下文窗口

那个 out[:50000] 尤其重要------它正是 Context 模块"窗口控制"的具体实现。 如果 ls -R 输出了 20 万字符全塞给 LLM,下一次调用可能直接超限。

Tool 模块:四个工具,四双手

python 复制代码
def run_read(path: str, limit: int = None) -> str:
    try:
        lines = safe_path(path).read_text(encoding="utf-8").splitlines()
        if limit and limit < len(lines):
            lines = lines[:limit] + [f"... {len(lines) - limit} more"]
        return "\n".join(lines)[:50000]
    except Exception as e:
        return f"Error: {e}"

def run_write(path: str, content: str) -> str:
    try:
        fp = safe_path(path)
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content, encoding="utf-8")
        return f"Wrote {len(content)} bytes"
    except Exception as e:
        return f"Error: {e}"

def run_edit(path: str, old_text: str, new_text: str) -> str:
    try:
        fp = safe_path(path)
        content = fp.read_text(encoding="utf-8")
        if old_text not in content:
            return f"Error: Text not found in {path}"
        fp.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
        return f"Edited {path}"
    except Exception as e:
        return f"Error: {e}"

三个文件工具,各有讲究:

run_read 的"省略提示"

python 复制代码
lines = lines[:limit] + [f"... {len(lines) - limit} more"]

读文件时如果超了 limit,不是简单截断,而是在末尾加一行提示"还有 N 行没显示"------这样 LLM 知道"内容被截了",可以选择再读一次,而不是误以为文件就这么长。

run_write 的自动建目录

python 复制代码
fp.parent.mkdir(parents=True, exist_ok=True)

parents=True 自动创建多级父目录,exist_ok=True 目录已存在也不报错。一行代码解决"目录不存在"这个高频报错。

run_edit 的"只替换第一处"

python 复制代码
content.replace(old_text, new_text, 1)

第三个参数 1 是关键------str.replace 默认替换全部匹配,传 1 就只替换第一个。为什么?因为 LLM 想改的是"某一段",如果这段文本在文件里出现多次,全替换可能误伤。"找不到就报错,不猜" 也是这个思路:

python 复制代码
if old_text not in content:
    return f"Error: Text not found in {path}"

注意这三个函数全部用 try/except 兜底,出错时返回字符串而不是抛异常。这是 Harness 的常见做法------错误信息本身就是要喂给 LLM 的上下文。告诉它"文件不存在",它就知道该先创建文件。

工具声明:给 LLM 看的说明书

python 复制代码
CHILD_TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "bash",
            "description": "Run a shell command.",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {"type": "string"}
                },
                "required": ["command"]
            }
        }
    },
    # read_file / write_file / edit_file 省略
]

PARENT_TOOLS = CHILD_TOOLS + [
    {
        "type": "function",
        "function": {
            "name": "task",
            # 给子Agent 分配完全独立的上下文
            "description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history.",
            "parameters": {
                "type": "object",
                "properties": {
                    "description": {"type": "string", "description": "Short description of the task"},
                    "prompt": {"type": "string", "description": "The task for the subagent to complete"}
                },
                "required": ["prompt"]
            }
        }
    }
]

这是今天最精妙的一处设计。

ini 复制代码
CHILD_TOOLS  = [bash, read_file, write_file, edit_file]        ← 子 Agent 的工具
PARENT_TOOLS = CHILD_TOOLS + [task]                            ← 主 Agent 多一个 task

子 Agent 有四个文件操作工具,但没有 task------它不能再往下派发子 Agent,否则会无限套娃。

主 Agent 则多一个 task 工具,description 写得极具信息量:

"Spawn a subagent with fresh context. It shares the filesystem but not conversation history."

"shares the filesystem but not conversation history"------共享文件系统,但不共享对话历史。一句话讲清了 Sub Agent 的本质:

共享什么 不共享什么
文件系统(都在同一个 WORKDIR) 对话历史(独立上下文)

这正是 readme 说的"上下文互不干扰",也是"主 Agent 上下文不受拖累"的实现方式。

工具分发:一本花名册

python 复制代码
TOOL_HANDLERS = {
    "bash": lambda **kw: run_bash(kw["command"]),
    "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"])
}

字典映射,工具名 → 执行函数。注释还贴心地把 Python 的 **kw 跟 JS 对比了一下:

"**kw js ...kw rest 运算符"

**kw 把 LLM 返回的参数字典展开成关键字参数------{"command": "ls"} 变成 run_bash(command="ls")。跟 JS 的展开运算符 ...kw 是一个思路。

注意 read_file 用的是 kw.get("limit")------因为 limit 是可选参数(Schema 里 required 只有 path)。必填用 kw["x"],选填用 kw.get("x"),这个区分很重要。

子 Agent:独立的小办公室

python 复制代码
def run_subagent(prompt: str) -> str:
    sub_messages = [{"role": "user", "content": prompt}]
    # 最多尝试30次
    # 独立的Agentic Loop
    for _ in range(30):
        response = client.chat.completions.create(
            model=MODEL,
            # 子Agent 自己的历史必须带上,否则 prompt 根本没发给模型
            messages=[{"role": "system", "content": SUB_SYSTEM}] + sub_messages,
            tools=CHILD_TOOLS,
            max_tokens=8000
        )
        msg = response.choices[0].message
        sub_messages.append(msg.model_dump())

        # OpenAI 协议里这个值是复数 tool_calls
        if response.choices[0].finish_reason != "tool_calls":
            break

        results = []
        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            handler = TOOL_HANDLERS.get(func_name)
            output = handler(**args) if handler else f"Unknown tool {func_name}"
            results.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(output)[:50000]
            })
        sub_messages.extend(results)
    return msg.content or "(no summary)"

这段是 Sub Agent 的核心 ------注意它跟主循环长得几乎一样,因为它本身就是一个完整的、独立的 Agentic Loop

几个关键设计:

1. 独立的上下文(fresh context)

python 复制代码
sub_messages = [{"role": "user", "content": prompt}]

子 Agent 的对话历史从零开始------只有 main agent 传进来的那个 prompt。它看不到主 Agent 之前聊了什么,也不知道自己是被谁派来的。

这就是"业务隔离"------子 Agent 的上下文里没有主 Agent 的历史包袱。

代码注释还强调了一句:

"子 Agent 自己的历史必须带上,否则 prompt 根本没发给模型"

因为 LLM 是无状态的,sub_messages 必须在每轮请求里完整带上,否则子 Agent 会"失忆"。

2. 30 次上限

python 复制代码
for _ in range(30):

注释说:

"最多尝试 30 次 / 下标我不用,占位置"

for _ in range(30) 里的下划线是 Python 惯例------"这个变量我不关心"。这 30 次是循环安全阀:防止子 Agent 陷入无限工具调用。

3. 终止条件

python 复制代码
if response.choices[0].finish_reason != "tool_calls":
    break

这就是 readme 说的"判断任务终止条件"------只要模型不再要求调工具,就说明它给出最终答案了,退出循环。

注释还提了个细节:

"OpenAI 协议里这个值是复数 tool_calls"

tool_calls 是复数------因为一次响应里 LLM 可能同时要求调多个工具。

4. 只返回结论

python 复制代码
return msg.content or "(no summary)"

子 Agent 干了 30 轮活儿,最后只返回一段文本给主 Agent。中间那些工具调用细节、文件内容、报错信息------全部留在子 Agent 自己的上下文里,不给主 Agent 添乱。

or "(no summary)" 是兜底------万一模型返回空内容,也得给主 Agent 一个可读的结果,不然主 Agent 会困惑。

readme 对 Sub Agent 的描述在这里得到了完整验证:

"子 Agent 返回结果,有全新的上下文,不会干扰主 Agent 上下文。"

主循环:Agent 的心脏

python 复制代码
def agent_loop(messages: list):
    while True:
        response = client.chat.completions.create(
            model=MODEL,
            messages=[{"role": "system", "content": SYSTEM}] + messages,
            tools=PARENT_TOOLS,
            max_tokens=8000
        )
        msg = response.choices[0].message
        print(msg.content, "??")
        messages.append(msg.model_dump())
        if response.choices[0].finish_reason != "tool_calls":
            return
        results = []
        msg = response.choices[0].message
        if msg.tool_calls:
            results = []
            for tool_call in msg.tool_calls:
                func = tool_call.function
                args = json.loads(func.arguments)
                # 主Agent分任务
                if func.name == "task":
                    desc = args.get("description", "subtask")
                    prompt = args.get("prompt", "")
                    print(f"> task({desc}): {prompt[:80]}")
                    # 启动子Agent
                    output = run_subagent(prompt)
                else:
                    # 主Agent 也可以自己干活
                    handler = TOOL_HANDLERS.get(func.name)
                    output = handler(**args) if handler else f"Unknown tool {func.name}"

                results.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": str(output)
                })
        messages.extend(results)

这就是 Loop 模块------readme 说的"核心控制层,调用大模型,分发工具调用,判断任务终止条件",逐条对上:

readme 描述 代码实现
调用大模型 client.chat.completions.create(...)
分发工具调用 for tool_call in msg.tool_calls:
判断任务终止条件 if finish_reason != "tool_calls": return
自主长时间干活 while True:

核心分支在 if func.name == "task"

python 复制代码
if func.name == "task":
    ...
    output = run_subagent(prompt)      # 派活给子 Agent
else:
    handler = TOOL_HANDLERS.get(func.name)
    output = handler(**args)           # 自己干

注释写得明白:

"主 Agent 分任务" / "启动子 Agent" / "只关注结果" / "主 Agent 也可以自己干活"

主 Agent 有两副面孔------简单活儿自己干(读个文件、跑个命令),复杂活儿派给子 Agent。这个"分派 or 自办"的决策完全由 LLM 自己根据任务性质决定,代码不干预。

这就是 Agent 和 Workflow 的分野------Workflow 里"谁来干"是写死的,Agent 里是 LLM 现场决定的。

两个 System Prompt:角色隔离

python 复制代码
# 主Agent 系统提示
SYSTEM = (
    f"You are a coding agent at {WORKDIR}."
    "Use task for focused exploration or a self-contained subtask."
)

SUB_SYSTEM = (
    f"You are a coding agent at {WORKDIR}."
    "Complete the given task, then return a concise final answer."
)

readme 说的"专属 SYSTEM prompt",就是这两行:

Prompt 角色定位 关键指令
SYSTEM 主 Agent(规划、分工) "Use task for focused exploration..."
SUB_SYSTEM 子 Agent(执行任务) "Complete the given task, then return a concise final answer."

三处细节:

1. 用 f-string 把 WORKDIR 嵌进去

两个 prompt 都以 f"You are a coding agent at {WORKDIR}." 开头------告诉 LLM 自己在哪个目录。 这样它调 bash 和读写文件时不会迷路。

2. 主 Agent 被明确引导使用 task

主 SYSTEM 里那句 "Use task for focused exploration or a self-contained subtask." 是在主动引导 LLM 使用子 Agent------不写这句,模型可能什么都自己干,Sub Agent 就白设计了。

3. 子 Agent 被要求"简洁收尾"

"return a concise final answer"------子 Agent 干了多少活都行,但给主 Agent 的回复必须简洁。这是上下文管理的最后一道防线:子 Agent 自己的上下文可以很脏,但传给主 Agent 的必须是干净结论。

还有个 Python 语法彩蛋,注释特意标了:

"python 隐式字符串拼接,括号里连续放多个字符串变量"

f"You are..." "Use task..." 两个字符串写在一起,Python 会自动拼接成一个------不需要 + 号。这就是为什么 f-string 只加在第一个上,第二个却能共享 WORKDIR 插值的上下文。

交互界面:一间能进的办公室

python 复制代码
if __name__ == "__main__":
    print("Subagent - fresh messages, final text returns")
    print("Enter a question, press Enter to sent. Type q to quit.\n")
    history = []
    while True:
        try:
            query = input("\001\033[36m\002s06 >> \001\033[0m\002")
        except (EOFError, KeyboardInterrupt):
            break
        print(query)
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({"role": "user", "content": query})
        agent_loop(history)

一个极简的交互式 CLI:

  • history 累积对话(这就是最基础的 Memory 模块------跨轮次保持在内存里)
  • input() 读用户输入
  • q / exit / 空行退出
  • EOFError / KeyboardInterrupt 优雅退出

那串 "\001\033[36m\002s06 >> \001\033[0m\002" 是 ANSI 转义序列------把提示符染成青色(\033[36m),\001 / \002 是 readline 库需要的边界标记(不加会导致光标位置计算错乱)。一个彩色提示符,藏着两个知识点。

注意 agent_loop(history)在主循环内部调用 的------每轮用户输入都调一次,而 history 一直在累积。这就是"多轮对话"的实现方式:Agent 的 Loop 是一层,用户交互的 Loop 是外面那层。


七、一张图收尾:六大模块在代码里的落点

scss 复制代码
agent.py(338行)
│
├── Environment 沙箱 ────────── safe_path() + WORKDIR
│                              路径校验、工作区隔离
│
├── Policy & Safety ─────────── dangerous 黑名单
│                              rm -rf / sudo shutdown 拦截
│
├── Tool 工具集 ─────────────── run_bash / run_read / run_write / run_edit
│                              + CHILD_TOOLS / PARENT_TOOLS 声明
│
├── Loop 主循环 ─────────────── agent_loop()  while True
│                              ReAct:调用→分发→判断终止
│                              run_subagent()  子 Agent 独立循环(30次上限)
│
├── Context 上下文管理 ───────── messages + SYSTEM + SUB_SYSTEM
│                              out[:50000] 窗口控制
│                              子 Agent 只 return msg.content(压缩)
│
├── Memory 记忆层 ───────────── history 列表(跨轮次)
│                              子 Agent 的 sub_messages(独立上下文)
│
└── Observability ───────────── print(msg.content) / print(f"> task(...)")
                                (最简版:日志输出)

六个模块,一个不少,全在这 338 行里。 当然这是教学版本的极简实现------真正的生产级 Harness(比如你用的 Claude Code、Codex)在这六个方向上都有几十倍的工程量,但骨架是同一个


八、给未来的自己:这份代码教会了什么

最后总结三层收获:

第一层:架构视角

Agent 不是"调用 LLM 的代码",而是六个模块协同的系统。Loop 是心脏、Tool 是手、Context 是记忆的调度台、Environment 是围栏、Memory 是笔记本、Observability 是监控。缺一个,"可靠、持续"就打折扣。

第二层:工程取舍

代码里处处是取舍------黑名单拦危险命令(简单但脆弱)、输出截断 5 万字符(防爆窗口但可能丢信息)、子 Agent 上限 30 轮(防死循环但可能不够用)。没有一个方案是完美的,都是权衡的结果。 这才是"Engineering"这个词的分量。

第三层:职业方向

readme 开头的两个岗位------"AI 应用开发工程师(Agentic RAG 应用)"、"AI Agent 开发工程师(Harness Agent FDE)"。学完这节课你会发现,这两个岗位的核心能力,不是"会调 API",而是能设计出上面这套系统


PS:这节课学完,再看任何 Agent 产品,你都能拆成六个问题问它------你的主循环怎么设计的?工具集有哪些?上下文怎么压缩?沙箱怎么隔离?记忆存在哪?出问题怎么查?能答上来的,是认真做的产品;答不上来的,大概就是"套了个壳的聊天框"。

相关推荐
linux_cfan2 小时前
videojs v10 源代码系列解读:14 · 谓词守卫:在运行时安全地调用能力
前端·javascript·音视频
kyriewen2 小时前
我扒了 10,221 条 JD:腾讯技术岗 75% 在要 AI
前端·人工智能·ai编程
郑州光合科技余经理3 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
IT_陈寒4 小时前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
Blanche15004 小时前
利用 RAG 为答疑机器人扩展知识范围
前端
天若有情6734 小时前
【纯前端小工具】公历生日转农历,批量查询每年农历生日对应的公历日期(GitHub Pages在线直接用)
前端·javascript·github pages·农历转换·lunisolar·网页小工具
SoonITer4 小时前
怎样构建一个 Agent-friendly 的网站
前端·agent
颜进强4 小时前
01 · NestJS 是什么:用途、解决什么问题、与热门框架对比
前端·后端·ai编程
wendZzoo4 小时前
前端工程师的 3D 第一课:一个模型如何进入网页
前端