通用智能体:Harness Agent 的全栈最小可用实现


Harness Agent Cover

前言:从“和文件对话”到“通用智能体”

之前写过一篇和文件对话智能体(LLM Agent),记录了一次非常奇妙的开发体验:把一个想做很久的 Chat with Files Agent 扔给 Cursor Plan 模式,给出 Next.js、LangChain、LibSQL、文件加载器、可视化等技术选型偏好,最后很顺滑地拿到一个能跑起来的应用。那篇文章的情绪更像是“AI 编程时代的震撼记录”:我描述需求,模型规划任务,工具执行代码,最后项目出现了。

但那种震撼之后,很快会回到一个更工程化的问题:如果我们不是偶然做出一个应用,而是想把“AI 帮我做软件工程”变成一个可复用、可验证、可扩展的系统,那么它应该长什么样?

单纯的 workflow 式 Agent 很容易把问题讲得漂亮。譬如先分析需求,再制定计划,再写代码,再运行测试,再总结结果。这个流程在图上非常顺:

但真实的软件工程不是一个只存在于 prompt 里的流程。它需要读写真实文件、执行真实命令、观察真实输出、生成真实产物、保留真实过程,并且所有副作用都要被边界约束住。也就是说,我们不只需要一个“会想”的 Agent,还需要一个可以把 Agent 放进去运行的工程环境。

这就是我这次想做 Harness Agent 的出发点。

harness 这个词在工程里很常见,尤其是 test harness。测试夹具不替代被测系统本身,但它会为被测系统提供输入、依赖、运行环境、观测点和断言。把这个思路挪到智能体工程里,Harness Agent 要解决的不是“写一个固定 workflow”,而是“给模型提供一个可执行、可观察、可验证、可收敛的工程夹具”。

本文会围绕新建的 zero/harness-agent 原型项目展开,聊聊一个最小可用的通用智能体如何从架构和工程实现上搭起来。

Workflow Agent 的局限

很多 Agent 框架最容易落入的问题,是把智能体理解成一张流程图。流程图当然重要,它能表达意图,能约束阶段,也能帮助人理解系统。但软件工程任务的核心困难往往不在“流程是否存在”,而在“流程里的每一步是否能被真实执行和验证”。

workflow 的主要问题有几个:

  • 上下文不稳定:模型以为自己知道项目结构,但没有经过真实文件系统确认。
  • 执行不可验证:模型说“已经完成”,但没有命令输出、测试结果或 artifact 证明。
  • 副作用不可控:一旦开放文件和命令能力,如果没有 workspace boundary,就很难知道 Agent 影响了哪里。
  • 能力不可组合:本地工具、MCP、skills、provider profile、artifact writer 往往各自为政,缺少统一的能力面。
  • 过程不可回放:用户只能看到最终回答,看不到工具调用、错误、产物创建和中间观察。

对软件工程来说,这些问题都不是体验细节,而是系统能不能被信任的基础。

Harness Engineering 的抽象

我理解的 harness engineering,大致包含五类东西:

  1. 执行环境:Agent 运行在哪里,能访问哪些文件,能执行哪些命令。
  2. 能力注册:有哪些工具、MCP server、skills、provider profile 可以被启用。
  3. 事件观测:Agent 每一步做了什么,能不能被 UI 展示和持久化。
  4. 产物管理:最终生成的不只是文本回答,还包括 Markdown、HTML、CSV、XLSX、PDF 等可以预览和下载的 artifact。
  5. 验证机制:最小闭环不是靠口头证明,而是靠单元测试、集成测试、smoke test 去证明。

如果用 test harness 类比,它和 agent harness 的关系大概是这样:

所以 Harness Agent 这个项目不是想重新发明一个“更聪明的 Agent 算法”,也不是一开始就做复杂的多 Agent 协作。它优先解决的是工程智能体的运行底座:让一个本地单用户的 Agent 能在浏览器 UI 中被操作,在本地 server 中执行受控任务,在 workspace 中产生可追踪的副作用,并且把过程和结果都持久化下来。

最小可用闭环

一个最小可用的 Harness Agent,不需要一开始就有云端账户、多租户权限、复杂调度系统、容器沙箱、插件市场和完整商业化体系。MVP 的边界越清晰,越容易把关键链路做实。

这次原型的最小闭环是:

它要满足几个朴素但关键的条件:

  • 用户可以创建一个 project,并指定 workspacePath
  • 所有文件读写、命令执行、artifact 生成都被限制在这个 workspace 里。
  • 用户可以通过浏览器发起一次 session task。
  • runtime 可以根据 settings 装配 provider、tools、MCP、skills。
  • 执行过程会产生结构化 AgentEvent
  • 最终回答会被保存为 conversation message。
  • 生成的 artifacts 会落在 .harness-agent/artifacts/<session-id>/ 下,并通过 API 预览或下载。
  • 没有 API key 时,也能通过默认 local runtime 生成 demo artifacts,证明 UI、server、storage、artifact preview 是闭环的。

这里最重要的不是“它能聊得多聪明”,而是它具备一个软件工程智能体最基本的身体结构。

分层架构

项目采用 pnpm workspace,所有包都放在 packages/ 下。整体上分为 webservercoretoolsmcp-fixturesexample-skills 几个部分。

这种分层有一个好处:core 尽量保持和 Web 框架无关,server 负责 HTTP、存储、SSE、settings、project lifecycle,tools 负责本地副作用,web 只关心控制台体验。后续无论是换 provider、接入真实 MCP transport,还是把 artifact preview 做得更强,都有明确的修改边界。

数据模型

MVP 里最重要的实体其实不多:ProjectTaskSessionConversationMessageArtifactGlobalSettings。这些实体共同描述了“一个任务在哪里发生、由谁驱动、产生了什么过程和结果”。

Project 是这里的核心实体。普通聊天应用里,conversation 往往是第一实体;但工程智能体里,workspace 应该先于 conversation。因为对工程任务而言,“在哪个项目里做事”比“对话叫什么名字”更重要。

执行时间流

一次任务从用户点击发送到最终展示结果,大致会经过下面的时间流:

这个时间流里有两个值得强调的点。

第一,工具观察会进入 provider 上下文。譬如用户问“当前目录有哪些文件”,runtime 会先通过 filesystem.list 拿到真实 workspace 的目录条目,再把这个 observation 作为 system message 交给 provider。模型不再凭空猜测项目结构,而是基于工具结果回答。

第二,artifact 是一等产物。Agent 不只是输出一段文本,还可以把 Markdown、HTML、CSV、XLSX、DOCX、PPTX、PDF、JSON、text 等文件写入 workspace,并通过 server 的 preview/download API 暴露给前端。

Session 状态机

TaskSession 的状态设计很克制:queuedrunningcompletedfailedcancelled。它不像复杂工作流引擎那样描述每一个业务阶段,而是只描述一次 agent task 在 runtime 中的生命周期。

状态机的价值不在复杂,而在可回放。只要 session status、conversation messages、event log、artifact ids 都被持久化,前端就可以随时重新展示这次任务的过程和结果。对于工程智能体,这种过程可见性很重要,因为用户关心的不只是“答案是什么”,还关心“它到底做了什么”。

Workspace Boundary

本地工程智能体最危险的能力,是文件系统和命令执行。能力本身必须存在,否则 Agent 只能停留在咨询层;但能力必须被边界约束,否则风险不可接受。

harness-agent 的边界策略是:每个 project 有一个 workspacePath,所有本地副作用都从这个路径开始解析,任何试图逃逸 workspace 的路径都会被拒绝。

这类边界不是生产级容器隔离,但对一个本地单用户 MVP 来说,它是第一道必须存在的工程约束。

命令执行也做了基本限制:

  • 默认 cwd 是 project workspace。
  • 指定 cwd 时同样需要通过 resolveProjectPath
  • 命令有超时控制,默认 30 秒。
  • stdout/stderr 有截断限制,默认 64KB。
  • 环境变量只带入有限集合,例如 PATHHOME

这不是为了制造安全感,而是为了把最小闭环中的风险收束到可理解的范围内。

Tool、MCP 与 Skills 的统一能力面

一个通用智能体不应该把能力硬编码在 prompt 里。它需要一个能力注册层,让内置工具、MCP 工具和 skills 都能以比较一致的方式参与 runtime。

当前 MVP 里,内置工具包括:

  • filesystem.read
  • filesystem.write
  • filesystem.list
  • filesystem.search
  • command.execute
  • web.fetch
  • web.search
  • artifact.write
  • skill.create

MCP 部分目前提供的是 deterministic fixture,用来覆盖 filesystem、web、artifact、skill creator 这些常见能力。它的意义不是“已经接入完整 MCP 生态”,而是先把 registry、settings、API、UI、测试的通路打通。

skills 部分则加载启用路径下的 SKILL.md,解析 namedescriptiontriggers 和正文指令。第一版只做简单触发:当任务文本命中 skill 的名称、描述或触发词时,把 skill instructions 注入上下文。这个策略很朴素,但足够证明本地技能文件可以进入 agent harness。

Provider Profiles

Provider 层的目标是把不同模型 API 的协议差异隔离起来。项目里定义了统一的 ProviderRequestProviderEvent,再分别转换到 OpenAI、Anthropic、OpenAI-compatible 协议。

默认 settings 里预置的是 DeepSeek 的 OpenAI-compatible profile,读取 DEEPSEEK_API_KEY。同时也保留 OpenAI、Anthropic 和本地 OpenAI-compatible endpoint 的扩展形态。

这里还有一个故意设计的 fallback:没有 API key 时,默认 local runtime 仍然可以运行一个 artifact demo。它会在 workspace 中生成 Markdown、HTML、CSV、XLSX、DOCX、PPTX、PDF 等一组本地产物。这个 fallback 不追求智能,但能验证全栈链路:

  • project 创建是否正常;
  • session 是否能持久化;
  • event 是否能记录;
  • artifact writer 是否能写文件;
  • artifact route 是否能 preview/download;
  • web console 是否能看到结果。

这就是 MVP 的工程意义。

Artifact 是回答之外的第二输出

普通聊天应用里,assistant message 是主要产物。工程智能体里,message 往往只是过程说明,真正可交付的东西应该落在文件系统里。

当前 artifact writers 支持的类型包括:

  • Markdown
  • HTML
  • CSV
  • XLSX
  • DOCX
  • PPTX
  • PDF
  • JSON
  • Text
  • Image metadata

这套机制让 Agent 可以从“答复一个问题”进化到“交付一个文件”。譬如写一份 Markdown 计划、生成一份 CSV 数据、产出一个 HTML 报告、导出一个 PDF brief。对于软件工程工作来说,这比只返回聊天内容更接近真实交付。

Web Console

前端不是一个 landing page,而是一个工作台。它需要让用户完成几个真实动作:

  • 创建或选择 project。
  • 选择 provider profile。
  • 发起一次 task。
  • 查看 conversation。
  • 展开 tool call、tool result、skill match、artifact created 等 process events。
  • 在侧边栏预览 artifacts。
  • 管理 projects、runs、artifacts、settings。

这里的 UI 重点不是炫技,而是把 Agent 的工作过程呈现出来。一个软件工程智能体如果只能看到最后答案,用户很难建立信任;如果能看到它调用了哪些工具、拿到了什么结果、生成了哪些 artifact,用户就能在必要时接管和判断。

MVP 的构建甘特图

从实现顺序上看,这个项目适合按自底向上的方式做。先有 monorepo 和类型边界,再做 core policy 和 provider conversion,然后是 registry、tools、server、web,最后用 smoke test 把闭环拉起来。

这个甘特图看起来像一天完成,但它表达的不是日历意义上的工期,而是 MVP 的依赖顺序:底座先行,能力注册其次,界面最后接入,最后用 smoke test 证明系统闭环。

代码层面的几个关键点

1. Project Policy 是所有本地副作用的入口

文件系统、命令执行、artifact writer 和 artifact preview 都需要经过 project policy。这个设计让安全边界不散落在各个工具里。

export function resolveProjectPath(project: Project, requestedPath: string): string {
  const workspacePath = normalizeExistingOrPlannedPath(project.workspacePath);
  const candidatePath = isAbsolute(requestedPath)
    ? requestedPath
    : resolve(workspacePath, requestedPath);
 
  if (!isPathInsideProject(project, candidatePath)) {
    throw new Error(`Path is outside project workspace: ${requestedPath}`);
  }
 
  return normalizeExistingOrPlannedPath(candidatePath);
}

这个函数要处理两种路径:已经存在的真实路径,以及尚未创建的计划路径。后者对 artifact 和文件写入很重要,因为目标文件可能还不存在,但它的父目录和最终路径仍然必须被约束在 workspace 内。

2. Tool Registry 把能力调用标准化

ToolRegistry 本身很小,但它承担了一个关键职责:runtime 不直接知道每个工具怎么实现,而是通过统一 id 和 handler 调用。

export class ToolRegistry {
  private readonly tools = new Map<string, RegisteredTool>();
 
  register(definition: ToolDefinition, handler: ToolHandler): void {
    this.tools.set(definition.id, { definition, handler });
  }
 
  async call(toolId: string, context: ToolCallContext): Promise<unknown> {
    const tool = this.tools.get(toolId);
 
    if (!tool) {
      throw new Error(`Tool not found: ${toolId}`);
    }
 
    return tool.handler(context);
  }
}

这个 registry 后续可以继续扩展:内置工具、MCP tools、skill tools 都可以被 normalize 到类似的调用接口上。

3. AgentEvent 是 UI 和 runtime 的契约

当前事件类型包括:

export type AgentEvent =
  | { type: "session.started"; sessionId: string }
  | { type: "thinking"; summary: string }
  | { type: "message.delta"; messageId: string; text: string }
  | { type: "message.completed"; messageId: string }
  | { type: "skill.match"; skillName: string; path: string; reason: string }
  | { type: "tool.call"; callId: string; toolName: string; input: unknown }
  | { type: "tool.result"; callId: string; toolName: string; output: unknown }
  | { type: "artifact.created"; artifact: Artifact }
  | { type: "error"; message: string; recoverable: boolean }
  | { type: "session.completed"; sessionId: string };

这个事件流是 Harness Agent 的“时间线”。前端可以用它展示过程,server 可以用它做 replay,测试可以用它断言行为。相比只保存最终 assistant message,结构化事件让智能体的运行过程变得可检查。

4. Local Runtime 先证明闭环

完整的 provider tool-calling loop 会越来越复杂,但 MVP 阶段先做一件更重要的事:证明本地全栈闭环。

没有 API key 的情况下,default runtime 会生成一组本地 artifacts:

  • Local Task Summary.md
  • Local Task Preview.html
  • Local Task Data.csv
  • Local Task Workbook.xlsx
  • Local Task Document.docx
  • Local Task Deck.pptx
  • Local Task Brief.pdf

这让项目在没有模型服务的情况下也可以测试和演示。对一个工程底座来说,这是非常实用的取舍。

Smoke Test:用测试证明“能跑”

local-agent-smoke.test.ts 做的事情很直接:

  1. 创建临时 data dir。
  2. 创建临时 workspace。
  3. 通过 API 创建 project。
  4. 创建 session。
  5. 发送一条“生成本地闭环 smoke report”的消息。
  6. 验证 session 中出现 assistant message。
  7. 验证 artifact ids 包含 Markdown 和 PDF。
  8. 验证 artifact list 包含多种文件类型。
  9. 验证 Markdown preview 有用户输入内容。
  10. 验证 PDF 下载内容以 %PDF 开头。

这个 smoke test 的意义,是把“通用智能体”这个听起来很大的概念,压缩成一个可验证的最小链路。只要这条链路通了,后面扩展 provider、MCP、skills、artifact 类型和 UI 体验,才有稳定的地基。

和 Chat with Files Agent 的区别

回到开头提到的“和文件对话智能体”。Chat with Files Agent 是一个具体垂直场景:上传文件,解析内容,做 RAG,回答问题,生成图表或脑图。它的任务目标非常明确。

Harness Agent 更像是一个上层工程底座。它不预设唯一业务场景,而是先抽象出工程智能体需要的通用能力:

换句话说,Chat with Files Agent 可以成为 Harness Agent 之上的一个技能或场景;而 Harness Agent 要做的是让这类场景都能拥有统一的执行环境、边界控制、事件可见性和产物交付机制。

当前 MVP 的限制

必须诚实地说,现在这个项目还只是一个 MVP,不是生产系统。

当前限制包括:

  • 单用户本地原型,不包含账号、组织、权限和多租户。
  • workspace boundary 不是容器沙箱,不能替代系统级隔离。
  • MCP 目前主要是 fixture,用于打通能力面和测试,不等于完整 MCP 生态接入。
  • skill routing 只是简单 trigger matching,还不是复杂的技能规划系统。
  • provider tool-calling loop 还可以继续增强,尤其是多步工具调用、错误恢复、流式 tool result 和长期记忆。
  • session cancel 目前更像状态更新,还需要和 AbortController、子进程生命周期深度绑定。
  • artifact preview 已经有最小闭环,但更复杂的文件类型和交互式报告还可以继续强化。

这些限制并不影响 MVP 的价值。相反,它们说明这次原型真正完成的是“通用智能体底座的最小骨架”,而不是试图一次性完成所有宏大设想。

后续演进

接下来比较自然的演进方向有几条。

我最关心的还是 runtime 和 safety。工程智能体的能力越强,边界越重要;可执行能力越多,事件、测试、审计和回放越不能省。

小结

这篇文章的标题叫“通用智能体”,但这里的“通用”不是说它已经无所不能,而是说它抽出了工程智能体里比具体业务更底层的一组通用结构:

  • project workspace;
  • provider profile;
  • tool registry;
  • MCP registry;
  • skill registry;
  • artifact writer;
  • event stream;
  • session store;
  • workspace policy;
  • smoke test。

workflow 告诉 Agent “应该怎么做”,harness 告诉 Agent “能在哪里做、用什么做、做了什么、结果如何证明”。对软件工程场景来说,后者往往才是智能体从 demo 走向可用系统的关键。

如果说“和文件对话智能体”那篇记录的是 AI 编程工具带来的即时震撼,那么这次 harness-agent 更像是把震撼冷却下来之后的一次工程拆解:我们不只让模型写代码,也开始认真设计模型工作的工位、工具箱、日志、产物和验收方式。

这可能就是下一阶段软件工程智能体最值得投入的地方:不是追逐一个更漂亮的流程图,而是把 Agent 放进一个真正可执行、可观察、可验证的 harness 里。


评论 (0)

登录后即可发表评论