
前言:从“和文件对话”到“通用智能体”
之前写过一篇和文件对话智能体(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,大致包含五类东西:
- 执行环境:Agent 运行在哪里,能访问哪些文件,能执行哪些命令。
- 能力注册:有哪些工具、MCP server、skills、provider profile 可以被启用。
- 事件观测:Agent 每一步做了什么,能不能被 UI 展示和持久化。
- 产物管理:最终生成的不只是文本回答,还包括 Markdown、HTML、CSV、XLSX、PDF 等可以预览和下载的 artifact。
- 验证机制:最小闭环不是靠口头证明,而是靠单元测试、集成测试、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/ 下。整体上分为 web、server、core、tools、mcp-fixtures、example-skills 几个部分。
这种分层有一个好处:core 尽量保持和 Web 框架无关,server 负责 HTTP、存储、SSE、settings、project lifecycle,tools 负责本地副作用,web 只关心控制台体验。后续无论是换 provider、接入真实 MCP transport,还是把 artifact preview 做得更强,都有明确的修改边界。
数据模型
MVP 里最重要的实体其实不多:Project、TaskSession、ConversationMessage、Artifact、GlobalSettings。这些实体共同描述了“一个任务在哪里发生、由谁驱动、产生了什么过程和结果”。
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 的状态设计很克制:queued、running、completed、failed、cancelled。它不像复杂工作流引擎那样描述每一个业务阶段,而是只描述一次 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。
- 环境变量只带入有限集合,例如
PATH和HOME。
这不是为了制造安全感,而是为了把最小闭环中的风险收束到可理解的范围内。
Tool、MCP 与 Skills 的统一能力面
一个通用智能体不应该把能力硬编码在 prompt 里。它需要一个能力注册层,让内置工具、MCP 工具和 skills 都能以比较一致的方式参与 runtime。
当前 MVP 里,内置工具包括:
filesystem.readfilesystem.writefilesystem.listfilesystem.searchcommand.executeweb.fetchweb.searchartifact.writeskill.create
MCP 部分目前提供的是 deterministic fixture,用来覆盖 filesystem、web、artifact、skill creator 这些常见能力。它的意义不是“已经接入完整 MCP 生态”,而是先把 registry、settings、API、UI、测试的通路打通。
skills 部分则加载启用路径下的 SKILL.md,解析 name、description、triggers 和正文指令。第一版只做简单触发:当任务文本命中 skill 的名称、描述或触发词时,把 skill instructions 注入上下文。这个策略很朴素,但足够证明本地技能文件可以进入 agent harness。
Provider Profiles
Provider 层的目标是把不同模型 API 的协议差异隔离起来。项目里定义了统一的 ProviderRequest 和 ProviderEvent,再分别转换到 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
- 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.mdLocal Task Preview.htmlLocal Task Data.csvLocal Task Workbook.xlsxLocal Task Document.docxLocal Task Deck.pptxLocal Task Brief.pdf
这让项目在没有模型服务的情况下也可以测试和演示。对一个工程底座来说,这是非常实用的取舍。
Smoke Test:用测试证明“能跑”
local-agent-smoke.test.ts 做的事情很直接:
- 创建临时 data dir。
- 创建临时 workspace。
- 通过 API 创建 project。
- 创建 session。
- 发送一条“生成本地闭环 smoke report”的消息。
- 验证 session 中出现 assistant message。
- 验证 artifact ids 包含 Markdown 和 PDF。
- 验证 artifact list 包含多种文件类型。
- 验证 Markdown preview 有用户输入内容。
- 验证 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)
登录后即可发表评论