教程整体目标
通过逐章可运行的增量,理解 Agent 的模型、上下文、任务状态、规划、工具、Skills 与 Agent Loop 等关键组件,并从零构建一个由 Harness 约束、不依赖 Agent 框架的可用 Agent。
最小 Agent Loop
先用最少状态和停止条件完成可观察、可取消的模型循环。
预计 30 分钟本页目录
已经有 ModelRequest 和可替换的 Model 接口。
src/types.tssrc/harness.tssrc/index.tstests/harness.test.ts
这是教程的第一个最小 Agent Loop。它暂时只有内存状态和文本出口;模型只提出下一步意图,是否继续、允许多少步、如何记录结果以及何时停止,都由 Harness 决定。后面的状态章会在基础仓库闭环完成后,把这份最小记录升级成可恢复、可审计的状态机。
Step 1划清 Harness 的职责
先固定边界,避免把整个应用写进一个循环:
- CLI:解析参数、装配依赖、设置退出码;
- Context Builder:从状态生成本轮
ModelRequest; - Model Adapter:调用模型并把厂商响应转成领域对象;
- Planner:进阶阶段再加入;最小 Loop 不要求先构造复杂计划;
- Harness:推进状态机、执行停止策略、调度工具;
- Tool Executor:校验、授权、超时、执行与序列化;
- Reporter:输出最终答案和验证证据。
模型不能直接访问文件系统,也不能自己把状态标记为完成。它只能返回文本或工具调用建议。
Step 2定义运行契约
在 src/types.ts 加入明确的输入、输出和停止原因。上限应由调用方传入,测试时才能把它设得很小。
import type { Model } from "./model.js";export interface HarnessOptions { cwd: string; maxSteps: number; model: Model; signal?: AbortSignal; onEvent?: (event: RuntimeEvent) => void;}export interface MinimalAgentState { runId: string; task: string; status: "running" | "completed" | "failed" | "cancelled"; modelSteps: number;}export interface StopReasonMap { final_answer: true; max_steps: true; max_tool_calls: true; cancelled: true; model_error: true; invalid_model_output: true;}export type StopReason = keyof StopReasonMap;export interface AgentResult { status: MinimalAgentState["status"]; answer: string; stopReason: StopReason; state: MinimalAgentState;}export type RuntimeEvent = | { type: "run_started"; runId: string } | { type: "model_started"; step: number } | { type: "model_completed"; step: number } | { type: "run_stopped"; reason: StopReason };这里暂时命名为 RuntimeEvent,避免与状态章将要持久化的审计事件混为一谈。事件回调不应改变运行语义;它只负责日志、UI 流式更新或 tracing。
Step 3实现第一个受控循环
创建 src/harness.ts。本章尚未接入工具,所以模型必须给出非空最终文本;下一章会在同一循环中加入工具分支。
import { randomUUID } from "node:crypto";import { buildModelRequest } from "./context.js";import type { AgentResult, HarnessOptions, MinimalAgentState, StopReason } from "./types.js";export async function runAgent( task: string, options: HarnessOptions,): Promise<AgentResult> { const state: MinimalAgentState = { runId: randomUUID(), task, status: "running", modelSteps: 0, }; options.onEvent?.({ type: "run_started", runId: state.runId }); try { for (let step = 1; step <= options.maxSteps; step += 1) { if (options.signal?.aborted) return stop(state, "cancelled"); const request = buildModelRequest({ task: state.task, cwd: options.cwd, toolNames: [], sources: [], }); options.onEvent?.({ type: "model_started", step }); const answer = (await options.model.generate(request)).trim(); options.onEvent?.({ type: "model_completed", step }); state.modelSteps = step; if (!answer) return stop(state, "invalid_model_output"); state.status = "completed"; return { status: "completed", answer, stopReason: "final_answer", state }; } return stop(state, "max_steps"); } catch { const reason = options.signal?.aborted ? "cancelled" : "model_error"; return stop(state, reason); }}function stop(state: MinimalAgentState, reason: StopReason): AgentResult { state.status = reason === "cancelled" ? "cancelled" : "failed"; return { status: state.status, answer: "", stopReason: reason, state };}Step 4统一处理失败与取消
网络超时、限流和无效响应不能变成未处理异常。只在模型适配器中重试可恢复错误,并使用有上限的指数退避;Harness 负责把最终失败映射成稳定的 stopReason。
至少测试这些分支:模型抛错、返回空文本、达到步骤上限、收到 AbortSignal。日志可以记录错误类型、请求 ID 和重试次数,但不要记录 API Key 或未脱敏的完整上下文。
Step 5让 CLI 只负责装配
更新 src/index.ts。入口负责信号处理和退出码,不了解循环细节。
import { runAgent } from "./harness.js";import { createModel } from "./model-factory.js";const task = process.argv.slice(2).join(" ").trim();if (!task) throw new Error("请提供任务");const controller = new AbortController();process.once("SIGINT", () => controller.abort());const model = createModel();const result = await runAgent(task, { cwd: process.cwd(), maxSteps: 12, model, signal: controller.signal, onEvent: (event) => console.error(JSON.stringify(event)),});console.log(result.answer);process.exitCode = result.status === "completed" ? 0 : 1;