脚手架工程(Harness Engineering):大模型应用的编排与基础设施构建

本文面向读者

你已经理解了 提示词工程 如何定义单次生成的上限,上下文工程 如何管理记忆与知识的注入,循环工程 如何建立”评估-反馈-修正”的迭代闭环。现在你面临一个更深层的问题:这些能力如何被整合进一个真正可运行、可维护、可演进的系统中?

写作目标:不是再讲一遍如何调 Prompt 或者如何搭 RAG——而是从架构师的视角,揭示大模型应用背后那条看不见的”脊梁”:将 Prompt、Context、Loop 三大工程粘合在一起的编排层与基础设施层。这是”四大工程”体系中的最后一环,也是从”能用”到”好用”的关键一跃。

前置阅读


1. 引言:为什么 LLM 需要”脚手架”?

1.1 一个令架构师不安的场景

生产痛点:当 LLM 调用散落在业务代码的每个角落

假设你正在开发一个智能客服系统。最初的需求很简单:用户提问 → 调用 LLM → 返回答案。你很自然地写下:

def answer_question(user_input: str) -> str:
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": API_KEY},
        json={"model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": user_input}]}
    )
    return response.json()["content"][0]["text"]

三个月后,你的代码库变成了这样:

  • 37 个不同的文件中散布着 requests.post(llm_api) 调用
  • 每个调用使用不同的 temperature、max_tokens、system 参数
  • 没有统一的 Token 计费统计(财务问你这个月花了多少钱,你只能估算)
  • 某个 API 版本升级导致 3 个模块的输出格式全部崩坏,你花了 4 个小时逐一修复
  • PM 问”用户满意度有变化吗”,你无法回答——因为你没有任何埋点

这不是 LLM 的问题。这是架构的问题。

1.2 四大工程的分工:定位脚手架工程

在进入正文之前,我们必须精准界定”四大工程”各自的职责边界。这不是学术上的咬文嚼字——边界模糊会导致架构层面的职责重叠与代码腐化。

flowchart TB
    subgraph FourPillars["大模型应用四大工程支柱"]
        direction TB

        subgraph Prompt["🎯 提示词工程 (Prompt Engineering)"]
            P1["决定单次生成的质量上限"]
            P2["接口协议设计"]
            P3["版本管理与 A/B 测试"]
        end

        subgraph Context["🧠 上下文工程 (Context Engineering)"]
            C1["管理记忆、知识与数据注入"]
            C2["RAG 检索策略"]
            C3["上下文窗口经济学"]
        end

        subgraph Loop["🔄 循环工程 (Loop Engineering)"]
            L1["评估-反馈-修正闭环"]
            L2["数据飞轮与埋点"]
            L3["LLM CI/CD"]
        end

        subgraph Harness["🏗️ 脚手架工程 (Harness Engineering)"]
            H1["将 Prompt + Context + Loop 粘合为可运行系统"]
            H2["路由编排与状态机控制"]
            H3["中间件、拦截器与错误兜底"]
            H4["工具与外部系统的安全集成"]
            H5["运行时监控与可观测性基础设施"]
        end
    end

    Harness --> Prompt
    Harness --> Context
    Harness --> Loop

    style Harness fill:#fff3e0,stroke:#f57c00,stroke-width:3px
    style Prompt fill:#e3f2fd,stroke:#1565c0
    style Context fill:#e8f5e9,stroke:#2e7d32
    style Loop fill:#fce4ec,stroke:#c62828

架构重点:脚手架工程的定位

如果说 Prompt 是大脑(决定思考质量)、Context 是记忆(决定信息丰富度)、Loop 是免疫系统(检测并修正错误)——那么 Harness 就是骨骼、神经和循环系统:它不生产”智能”,但决定了智能能否在真实环境中存活。

没有脚手架,Prompt 只是一段文本,Context 只是数据检索,Loop 只是离线脚本。脚手架让它们成为一个活的系统。

1.3 核心定义:控制面与数据面的分离

概念解析:什么是 Harness / 编排工程?

在大模型应用的架构中,我们引入一个经典的分布式系统思想——控制面(Control Plane)与数据面(Data Plane)的分离:

  • 数据面(Data Plane):LLM 的推理过程本身——输入 Prompt → 模型计算 → 输出 Token。这是”智能发生的地方”,本质上是概率性的、非确定性的。
  • 控制面(Control Plane):包裹在数据面外层的所有基础设施——路由决策、状态管理、工具的注册与调度、中间件拦截、监控埋点、错误恢复、降级策略。这是”纪律执行的地方”,必须是确定性的、可预测的。

脚手架工程的核心命题就是:如何设计一个健壮的控制面,来管理和约束一个非确定性的数据面。

flowchart LR
    subgraph User["用户请求"]
        U["输入"]
    end

    subgraph ControlPlane["🔧 控制面 (Control Plane) — 脚手架工程"]
        direction TB
        CP1["路由 & 意图识别"]
        CP2["中间件链"]
        CP3["状态机引擎"]
        CP4["工具调度器"]
        CP5["断路器 & 降级"]
        CP6["监控 & Trace"]
    end

    subgraph DataPlane["🧠 数据面 (Data Plane) — LLM 推理"]
        DP1["Prompt 组装"]
        DP2["模型推理"]
        DP3["输出解析"]
    end

    U --> CP1 --> CP2 --> CP3 --> CP4
    CP4 --> DP1
    DP1 --> DP2 --> DP3
    DP3 --> CP5
    CP5 --> CP6
    CP6 -->|"响应"| U

    style ControlPlane fill:#fff3e0,stroke:#f57c00,stroke-width:2px
    style DataPlane fill:#e3f2fd,stroke:#1565c0,stroke-width:2px

核心观点

LLM 只是引擎,脚手架才是底盘。 你可以换引擎(从 Claude 换到 GPT,从 Sonnet 升级到 Opus),但底盘的架构——路由、状态机、中间件、监控——不应该随引擎的变化而重写。这就是脚手架工程的终极目标:让 LLM 成为系统中的一个”可替换组件”,而非系统的”全部”。

1.4 从”给 Agent 加插件”到”组装 Agent 运行时”

架构思考:Harness 的抽象跃迁

传统插件系统解决的问题通常是:“Agent 可以调用哪些额外工具?” 例如 MCP Server、Skill、浏览器、数据库、Shell。它们扩展的是 Agent 的能力边界。

更进一步的 Harness 工程要解决的问题是:“Agent 自身如何运行?” 也就是循环(Loop)、会话(Session)、上下文策略(Context Policy)、记忆(Memory)、沙盒(Sandbox)、文件系统(Filesystem)、编排(Orchestration)、UI 是否也能成为可替换、可组合、可观测的组件。

这两种抽象的差别非常大:

抽象层级典型问题工程对象
工具 / MCPAgent 能做什么?外部能力与系统调用
技能(Skill)Agent 知道怎么做什么?任务方法、领域经验、工作流片段
上下文策略(Context Policy)Agent 本轮应该看到什么?上下文选择、压缩、淘汰、缓存
循环(Loop)Agent 如何决定下一步做什么?ReAct、Plan-and-Execute、CodeAct、自定义控制算法
脚手架(Harness)Agent 如何被组织、约束和恢复?状态、权限、沙盒、日志、回放、编排

如果只有 Tool / MCP 插件化,Agent 仍然是”一个框架写死的程序 + 一些外部扩展”。如果 Loop、Context、Session、Sandbox 都能插件化,Agent 就更接近:

Agent = 一组插件按照某种拓扑组合出来的运行时

这也是为什么脚手架工程越来越像系统软件:模型像 CPU,上下文窗口像内存,外部记忆像虚拟内存,工具调用像系统调用,沙盒像进程隔离,Loop 像调度器,执行轨迹像调试器与审计日志。

架构重点:能力优先,而不是流程优先

固定业务流程适合流程优先(Workflow-first):先设计 A → B → C 的节点,再把模型塞进节点。但通用 Agent 更适合能力优先(Capability-first):先定义 Agent 拥有哪些能力、每种能力的权限边界、运行预算、失败策略和可观测信号,再由 Loop 在运行时动态组合这些能力。

对通用 Agent 来说,过早写死 Workflow 会限制模型的探索空间;没有 Capability 边界又会让模型的副作用失控。Harness 的价值就在于同时提供自由探索的空间和不可越过的工程边界。


2. 脚手架工程的核心组件

2.1 路由与状态机:从非确定性输出到确定性控制流

2.1.1 路由层:意图识别与任务分发

脚手架工程的第一道关卡是路由。大模型可以处理几乎任何类型的输入——但你的业务系统不能只有一个巨大的 handle_everything() 函数。路由层的职责是将用户的模糊输入映射到确定的执行路径。

架构思考:路由的三层分级策略

L1: 规则路由(确定性)
   ↓ 匹配不到
L2: 语义路由(LLM 分类)
   ↓ 置信度不足
L3: 通用兜底(默认 Agent)

核心原则:能用规则解决的,不要动用 LLM。 每一次不必要的 LLM 调用都是延迟 + 成本 + 不确定性。路由层最经典的工程实践是:先用正则/关键词做第一层过滤,命中率通常可达 60-70%;未命中的再交给一个轻量级分类模型(或小参数 LLM)做语义路由;最后再交给主力模型。

flowchart TB
    Input["用户输入"] --> RuleRouter{"规则路由<br/>(关键词 / 正则 / 前缀匹配)"}

    RuleRouter -->|"'部署' in input"| DeployAgent["部署 Agent"]
    RuleRouter -->|"'查询' in input"| QueryAgent["数据查询 Agent"]
    RuleRouter -->|"'告警' in input"| AlertAgent["告警处理 Agent"]
    RuleRouter -->|"未命中"| SemanticRouter{"语义路由<br/>(轻量 LLM 分类)"}

    SemanticRouter -->|"意图: 代码生成<br/>置信度: 0.92"| CodeAgent["代码生成 Agent"]
    SemanticRouter -->|"意图: 文档搜索<br/>置信度: 0.85"| DocAgent["文档搜索 Agent"]
    SemanticRouter -->|"置信度 < 0.7"| DefaultAgent["通用对话 Agent<br/>(降级:提示用户明确意图)"]

    style RuleRouter fill:#c8e6c9,stroke:#2e7d32
    style SemanticRouter fill:#fff9c4,stroke:#f57f17
    style DefaultAgent fill:#ffcdd2,stroke:#c62828

2.1.2 状态机:模型的”纪律层”

架构重点:为什么纯 Prompt 驱动的 Agent 不够?

在 Agent 实践指南 中,我们详细讨论了 ReAct 模式的三大工程缺陷:行为不可预测、状态分散、无法中断恢复。这里从架构视角再做一次提炼:

脚手架工程的核心设计范式是:用确定性的状态机控制流程骨架,用概率性的 LLM 填充每个状态内的”智能行为”。

这个范式有三个关键推论:

  1. 状态转移不由 LLM 决定。 状态机中的 EXECUTING → VALIDATING → RETRYING → COMPLETED 这类流转是由代码明确定义的,LLM 的职责仅限于在单个状态内部进行推理(如”校验阶段:这个 SQL 查询结果是否符合预期?”)。

  2. 状态是可持久化、可恢复的最小单元。 当 Agent 在 EXECUTING 状态崩溃时,检查点机制可以将其恢复到该状态的重入口——而非从 INIT 重新开始。

  3. 每个状态都有明确的”合法操作白名单”。 在 VALIDATING 状态下,Agent 不应该被允许调用部署工具——状态的边界就是权限的边界。

工程避坑:状态粒度的"金发女孩原则"

状态不要太粗也不要太细。太粗(如只有 START / EXECUTING / DONE 三个状态)等于没做状态机——你的 Agent 仍然在三个状态之间”自由发挥”。太细(如 OPEN_FILE → READ_LINE_1 → READ_LINE_2 → ...)会导致状态爆炸,维护成本远超收益。

一个好的参考粒度:每个状态对应一个”可中断 + 可恢复 + 有独立业务含义”的原子操作。 例如”数据库迁移”是一个状态,而非”连接数据库 → 执行 DDL → 验证结果”三个状态——除非你的业务需要在 DDL 执行前后暂停以接受外部审批。

2.1.3 DAG 编排:当线性状态机不够用

对于复杂的多步骤 Agent 任务,单一的线性状态机不够表达——某些步骤天然可以并行,某些步骤有条件分支,某些步骤的依赖关系不是线性的。

架构思考:DAG(有向无环图)的工程价值

DAG 在大模型编排中的核心价值不是”能并行就并行”的性能优化——而是依赖关系的显式声明。当你用一个 DAG 描述任务流时:

  • 每个节点知道它的前置依赖是什么
  • 调度器知道哪些节点可以并发执行
  • 失败时,你精确定位到”哪个节点的哪个依赖未满足”
  • 重试时,你只需要从失败节点重新执行,而非整个流程
flowchart TB
    subgraph DAG["DAG 编排示例:自动化代码审查 Pipeline"]
        INIT["INIT: 接收 PR"] --> FETCH["FETCH: 拉取 Diff"]
        FETCH --> LINT["LINT: 静态分析<br/>(ESLint / Pylint)"]
        FETCH --> SEC["SECURITY: 安全扫描<br/>(CodeQL / Semgrep)"]
        FETCH --> DEPS["DEPS: 依赖分析<br/>(版本冲突检测)"]
        FETCH --> COMPLEXITY["COMPLEXITY: 复杂度分析<br/>(圈复杂度 / 文件行数)"]

        LINT --> AGG["AGGREGATE: 汇总所有检查结果"]
        SEC --> AGG
        DEPS --> AGG
        COMPLEXITY --> AGG

        AGG -->|"全部通过"| APPROVE["APPROVE: 自动批准 + 合并"]
        AGG -->|"有问题"| SUGGEST["SUGGEST: LLM 生成修复建议"]
        SUGGEST --> REPORT["REPORT: 生成审查报告"]
        APPROVE --> REPORT
    end

    style INIT fill:#e3f2fd,stroke:#1565c0
    style AGG fill:#fff3e0,stroke:#f57c00
    style REPORT fill:#c8e6c9,stroke:#2e7d32

最佳实践:DAG 中的 LLM 节点 vs 确定性节点

一个好的 DAG 编排实践是混合编排——并非所有节点都需要 LLM:

节点类型示例特点工程关注点
确定性节点ESLint 检查、安全扫描、依赖分析输入确定 → 输出确定,可精确断言性能优化(并行度、缓存)
LLM 推理节点修复建议生成、代码审查总结输入确定 → 输出近似,需要校验输出校验、Retry 策略、Token 预算
人工审核节点敏感操作审批、不确定性确认需要外部输入,可能长时间等待超时处理、异步回调、状态持久化

关键洞察:DAG 中的 LLM 节点应该像”纯函数”一样设计——输入由上游确定性节点提供(结构化数据),输出被下游确定性节点校验(JSON Schema 验证 + 业务规则检查)。LLM 永远不应该直接决定流程的分支走向。


2.2 中间件与拦截器:在 LLM 调用的前后插入工程逻辑

2.2.1 什么是 Agent 中间件?

概念解析

如果你做过 Web 后端开发,你对”中间件”这个概念一定不陌生——在 HTTP 请求到达 Controller 之前,和 Response 返回给客户端之后,框架允许你插入一系列拦截逻辑。Agent 中间件 是将这个思想移植到大模型调用链路上的架构模式:

请求进入 → [前置中间件链] → LLM 推理 → [后置中间件链] → 响应返回

中间件的关键在于:它是一段与业务逻辑解耦的、可组合的、对 LLM 透明的工程代码。

flowchart LR
    subgraph Request["请求进入"]
        R["用户输入 + 对话历史"]
    end

    subgraph PreMiddleware["前置中间件链 (Pre-Middleware)"]
        direction TB
        M1["🔐 敏感词过滤器<br/>检测并脱敏 PII / 密钥"]
        M2["📊 Token 预算检查器<br/>预估本次调用成本,超限则裁剪上下文"]
        M3["💾 缓存命中检查器<br/>语义相似度匹配,命中则直接返回缓存结果"]
        M4["🔀 模型路由器<br/>根据任务复杂度选择模型(便宜 vs 贵)"]
    end

    subgraph LLM["LLM 推理"]
        L["模型调用"]
    end

    subgraph PostMiddleware["后置中间件链 (Post-Middleware)"]
        direction TB
        P1["📐 JSON Schema 校验器<br/>解析输出,校验格式,失败则注入纠错指令重试"]
        P2["🛡️ 输出安全过滤器<br/>检查输出是否包含敏感信息、不当内容"]
        P3["💰 Token 计费记录器<br/>记录本次调用的实际 Token 消耗和费用"]
        P4["📈 质量评估器<br/>对输出质量打分,低于阈值则触发告警或重试"]
    end

    subgraph Response["响应返回"]
        Res["最终输出"]
    end

    R --> M1 --> M2 --> M3 --> M4 --> L
    L --> P1 --> P2 --> P3 --> P4 --> Res

    style PreMiddleware fill:#e8f5e9,stroke:#2e7d32
    style PostMiddleware fill:#fff3e0,stroke:#f57c00
    style LLM fill:#e3f2fd,stroke:#1565c0

2.2.2 前置中间件:在模型”看到”之前做好防护

① 敏感词过滤器(PII / Secret Detection)

工程避坑:永远不要把用户原始输入直接喂给 LLM

LLM API 调用意味着数据离开你的服务器。如果用户在输入中包含手机号、身份证号、API 密钥——这些数据会在你的 Token 日志、LangSmith Trace、甚至模型提供商的训练数据中留下痕迹。前置敏感词过滤不是可选项,是合规底线。

原始输入: "我的手机号是 13812345678,帮我查一下订单。API_KEY=sk-abc123xyz"
                ↓ [敏感词过滤器]
安全输入: "我的手机号是 [PHONE_REDACTED],帮我查一下订单。API_KEY=[SECRET_REDACTED]"

② Token 预算检查器(Budget Enforcer)

架构重点:Token 预算的工程化管理

Token 不是免费的——但如果你不做预算控制,你的 Agent 会在不知不觉中挥霍 Token。前置 Token 预算检查器的职责是:在每次 LLM 调用之前,预估本次调用的成本,如果超出预算则裁剪上下文或拒绝调用。

预算策略示例:

  • 普通对话:单次上限 4,000 tokens
  • 代码生成:单次上限 12,000 tokens
  • 复杂 Agent 推理链:总上限 100,000 tokens,单步上限 16,000 tokens

③ 语义缓存命中检查器(Semantic Cache)

最佳实践:语义缓存——性价比最高的性能优化

传统缓存的 key 是精确字符串匹配。但在 LLM 场景下,用户问”今天北京天气怎么样”和”帮我看看北京今天是什么天气”是同一个问题——精确匹配会 miss。语义缓存使用 Embedding 向量计算输入之间的语义相似度,相似度超过阈值(如 0.95)则直接返回缓存结果。

一个设计良好的语义缓存可以为高频问答场景节省 40-60% 的 LLM 调用成本。详见 上下文工程中的缓存策略。

2.2.3 后置中间件:在模型”说完”之后做好善后

① JSON Schema 校验器(Output Validator)

生产痛点:模型的 JSON 输出不可信任

模型输出的 JSON 可能被 Markdown 代码块包裹,可能包含尾逗号,可能缺少 required 字段,可能 key 名拼错。后置 JSON Schema 校验器的职责是:将模型的”大概符合格式”的输出,转化为下游代码可以安全消费的结构化数据。

校验失败时,中间件不直接返回错误——它构造一条纠错指令(“你输出的 JSON 缺少 name 字段,请补充后重新输出”),注入到下一次 LLM 调用的 Prompt 中,形成中间件级别的自动修复闭环。具体实现详见 Agent 实践指南中的 RobustJSONParser。

② 输出安全过滤器(Output Guard)

后置安全过滤是前置安全过滤的对称防线。即使 Prompt 中没有任何敏感信息,模型也可能在输出中”编造”出看似合理的敏感数据(如”你的默认密码是 admin123”)。输出安全过滤器检查生成的文本是否包含不应出现的内容模式。

③ Token 计费记录器(Cost Tracker)

架构重点:Token 计费是横向切面,不应散落在业务代码中

当你需要在 37 个文件中统计 LLM 调用成本时,你已经失败了。Token 计费应该作为中间件的一个切面(Aspect),在架构层面统一处理——每个 LLM 调用自动记录:调用时间、模型、输入 Token 数、输出 Token 数、费用、调用来源(哪个模块发起的)、Trace ID。

这不仅是财务需求,更是性能优化的基础数据——你会发现”这个 Agent 单次调用平均消耗 8,000 tokens,但 60% 的调用可以通过缓存解决”。

④ 质量评估器(Quality Scorer)

后置中间件的最后一环:对模型输出进行快速质量评估。这个评估器可以使用一个更小、更便宜的模型(如 Claude Haiku)对输出打分,低于阈值则触发告警、重试或降级。这是 循环工程中的 Eval 体系 在运行时的在线延伸。

2.2.4 中间件的编排:责任链模式

"""
中间件的责任链(Chain of Responsibility)编排——每个中间件独立、可组合、可替换。
这是脚手架工程中最优雅的架构模式之一。
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional
 
 
@dataclass
class LLMCallContext:
    """一次 LLM 调用在中间件链中的上下文"""
    messages: list[dict]
    model: str
    temperature: float
    metadata: dict[str, Any]  # 中间件可在此读写任意数据
 
 
class LLMMiddleware(ABC):
    """
    LLM 中间件基类。
 
    每个中间件实现 pre_process 和/或 post_process。
    返回 None 表示"继续链路";返回非 None 表示"截断链路,直接返回该结果"。
    """
 
    @abstractmethod
    def pre_process(self, ctx: LLMCallContext) -> Optional[str]:
        """前置处理:在 LLM 调用之前执行"""
        ...
 
    @abstractmethod
    def post_process(self, ctx: LLMCallContext, llm_response: str) -> Optional[str]:
        """后置处理:在 LLM 调用之后执行"""
        ...
 
 
class MiddlewareChain:
    """
    中间件责任链。
 
    调用流程:
    1. 依次执行所有中间件的 pre_process
       - 如果某个中间件返回非 None,截断链路,直接返回
    2. 执行 LLM 调用
    3. 反向依次执行所有中间件的 post_process
       - 如果某个中间件返回非 None,替换当前响应(可用于修复)
    """
 
    def __init__(self):
        self._middlewares: list[LLMMiddleware] = []
 
    def use(self, middleware: LLMMiddleware) -> "MiddlewareChain":
        self._middlewares.append(middleware)
        return self
 
    def execute(self, ctx: LLMCallContext, llm_call: callable) -> str:
        # 前置中间件
        for mw in self._middlewares:
            result = mw.pre_process(ctx)
            if result is not None:
                return result  # 截断链路(如缓存命中)
 
        # LLM 调用
        response = llm_call(ctx)
 
        # 后置中间件(可修改响应)
        for mw in self._middlewares:
            modified = mw.post_process(ctx, response)
            if modified is not None:
                response = modified  # 中间件修改了响应(如格式修复)
 
        return response

最佳实践:中间件的排序策略

中间件的执行顺序至关重要。一个推荐的前置中间件排序:

  1. 缓存检查器(最先执行——如果命中,后续全部跳过,节省最大成本)
  2. Token 预算检查器(在准备调用前做最后的预算核算)
  3. 敏感词过滤器(在数据离开服务器前的最后一道防线)
  4. 请求日志记录器(记录最终发送的请求内容,用于后续审计)

后置中间件推荐反向排序(后进先出),确保越底层的处理越靠近原始输出。


2.3 工具与协议集成层:安全、一致地连接外部世界

2.3.1 脚手架是 Agent 与外部世界的唯一桥梁

架构重点:工具调用的安全边界

当 Agent 调用一个工具时(如 deploy_to_production() 或 DELETE FROM users),这不再是一个”概率预测”——这是一个会产生真实世界副作用的确定性操作。 脚手架工程的核心职责之一是:在 LLM 的”意图”和真实世界的”操作”之间建立一道不可绕过的安全边界。

flowchart TB
    subgraph LLMSide["LLM 侧(非确定性)"]
        L1["模型推理: '用户想部署,我需要调用 deploy()'"]
        L2["模型输出: {tool: 'deploy', args: {env: 'prod', version: 'latest'}}"]
    end

    subgraph HarnessBoundary["脚手架安全边界(确定性)"]
        direction TB
        B1["🔍 参数校验<br/>env 必须是 'staging' | 'prod'<br/>version 必须符合 semver"]
        B2["🛡️ 权限检查<br/>当前用户是否有 prod 部署权限?<br/>当前时间是否在部署窗口内?"]
        B3["⏱️ 超时 & 熔断<br/>部署超时 300s<br/>连续 3 次失败则熔断 5 分钟"]
        B4["📝 审计日志<br/>记录: 谁、何时、什么操作、结果"]
    end

    subgraph RealWorld["真实世界操作"]
        R["执行 deploy() 并返回结果"]
    end

    L1 --> L2
    L2 --> B1 --> B2 --> B3 --> B4
    B4 --> R
    R -.->|"结果反馈"| L1

    style HarnessBoundary fill:#ffcdd2,stroke:#c62828,stroke-width:3px

2.3.2 工具注册表(Tool Registry):标准化工具接入

概念解析

工具注册表是脚手架工程中一个被严重低估的组件。它的核心设计理念是:工具的定义(给 LLM 看的描述)与工具的实现(实际执行的代码)应该分离,且注册表是两者之间的唯一映射。

"""
工具注册表的核心抽象——定义与实现分离,统一管理生命周期。
"""
from dataclasses import dataclass
from typing import Any, Callable
 
 
@dataclass
class ToolDefinition:
    """工具的定义层(暴露给 LLM 的接口描述)"""
    name: str
    description: str
    input_schema: dict[str, Any]  # JSON Schema
    # 安全元数据
    requires_approval: bool = False     # 是否需要人工审批
    is_idempotent: bool = False         # 是否幂等(可安全重试)
    max_timeout_seconds: int = 30       # 最大执行时间
    risk_level: str = "low"             # 风险等级:low / medium / high / critical
 
 
class ToolRegistry:
    """
    工具注册表——Agent 与外部系统的唯一交互通道。
 
    核心设计原则:
    1. 定义与实现分离:LLM 只看到 ToolDefinition,看不到实现细节
    2. 权限边界:每个工具声明自己的风险等级,高风险工具需要额外审批
    3. 生命周期管理:统一管理超时、重试、熔断——工具实现者不需要关心这些
    4. 审计追踪:每次工具调用自动记录,无论成功还是失败
    """
 
    def __init__(self):
        self._tools: dict[str, tuple[ToolDefinition, Callable]] = {}
 
    def register(self, definition: ToolDefinition, handler: Callable):
        self._tools[definition.name] = (definition, handler)
 
    def get_definitions(self) -> list[ToolDefinition]:
        """获取所有工具定义(用于注入 System Prompt)"""
        return [defn for defn, _ in self._tools.values()]
 
    def invoke(self, tool_name: str, args: dict, ctx: "AgentContext") -> Any:
        """
        执行工具调用——带有完整的脚手架保护。
 
        调用流程:
        1. 查找工具定义和实现
        2. 参数校验(根据 input_schema)
        3. 权限检查(根据 risk_level + 用户上下文)
        4. 超时控制(根据 max_timeout_seconds)
        5. 执行并记录审计日志
        6. 返回结果——LLM 永远只看到结构化输出,看不到异常堆栈
        """
        ...

工程避坑:工具返回的错误信息设计

当工具调用失败时,返回给 LLM 的错误信息需要精心设计。不是返回 "ConnectionError: timeout after 30s"——这会让 LLM 困惑,不知道下一步该做什么。而是返回结构化的、可执行的错误描述:

❌ 错误返回(LLM 无法据此调整行为):
"Error: upstream service returned 503"

✅ 正确的错误返回(LLM 可以据此调整行为):
{
  "success": false,
  "error_type": "SERVICE_UNAVAILABLE",
  "message": "部署服务当前不可用(上游返回 503),可能原因:服务正在重启或负载过高。",
  "suggested_action": "请等待 30 秒后重试,或使用 backup_deploy() 备用部署通道。",
  "recoverable": true,
  "retry_after_seconds": 30
}

工具错误信息的本质是”写给另一个模型看的 Prompt”——它需要包含失败原因、失败类型、建议的下一步行动。这直接决定了 Agent 能否自动从错误中恢复。

2.3.3 超时、重试与熔断:工具箱的可靠性三件套

架构重点:超时 + 重试 + 熔断是工具层的"安全网三层"

flowchart TB
    Start["工具调用开始"] --> TimeoutCheck{"超时检查?<br/>(硬性截止)"}

    TimeoutCheck -->|"未超时"| Execute["执行工具"]
    TimeoutCheck -->|"已超时"| TimeoutError["返回超时错误<br/>(含建议的重试策略)"]

    Execute -->|"成功"| Success["返回结果"]
    Execute -->|"失败"| RetryCheck{"重试策略检查?"}

    RetryCheck -->|"可重试 + 未达上限"| Backoff["指数退避等待<br/>1s → 2s → 4s → ..."]
    RetryCheck -->|"不可重试 或 已达上限"| CircuitCheck{"熔断检查?"}

    Backoff --> TimeoutCheck

    CircuitCheck -->|"错误率 < 阈值"| ReturnError["返回错误(含降级建议)"]
    CircuitCheck -->|"错误率 ≥ 阈值"| CircuitOpen["熔断器打开<br/>N 秒内拒绝所有调用<br/>直接返回降级结果"]

    style TimeoutCheck fill:#e3f2fd,stroke:#1565c0
    style CircuitOpen fill:#ffcdd2,stroke:#c62828
机制解决的问题关键参数脚手架中的位置
超时(Timeout)工具调用一直不返回,Agent 无限等待硬超时(30s-5min,取决于工具类型)工具调度器层——每个工具注册时声明 max_timeout_seconds
重试(Retry)网络抖动、临时故障导致的偶发失败最大重试次数(3)、退避策略(指数)、幂等性要求工具执行器层——根据 is_idempotent 决定是否允许重试
熔断(Circuit Breaker)下游服务持续不可用,重试不仅无效还加重负载错误阈值(如 5 次失败)、熔断时间(如 5 分钟)、半开探测工具注册表层——保护 Agent 和下游服务双方

最佳实践:熔断器的"半开"状态

熔断器不是”全开”或”全关”的二元状态。当你连续 5 次调用失败、熔断器打开后,你不应该永远拒绝请求——下游服务可能已经恢复了。经典的三态熔断器:

  • 关闭(Closed):正常工作,调用通过,同时记录失败次数
  • 打开(Open):达到失败阈值,所有调用直接拒绝(不经过网络),持续 N 秒
  • 半开(Half-Open):N 秒后,允许一次探测调用——如果成功,恢复到关闭状态;如果失败,回到打开状态

半开状态是熔断器自我修复的关键。 没有它,你只是把一个运行时故障变成了一个需要人工重启的配置故障。

2.4 Loop 插件化:把 Agent 的控制算法变成一等公民

2.4.1 为什么 Loop 不应该藏在框架内部

很多 Agent 框架把核心循环写死在运行时里:

while True:
    context = build_context()
    response = model(context)
 
    if response.tool_calls:
        results = execute_tools(response.tool_calls)
        append_results(results)
    else:
        return response

这个循环看起来简单,但真正上线后会不断塞入:

  • Context 压缩与淘汰
  • Tool 权限与审批
  • Retry / Timeout / Circuit Breaker
  • Human-in-the-loop Gate
  • Subagent 调度
  • Token Budget
  • Session Checkpoint
  • Termination 判断
  • Trajectory 记录与 Replay

当这些逻辑全部堆进一个 AgentRuntime.run() 时,Loop 就不再是业务代码里的 while 循环,而是整个 Agent 的控制算法。如果控制算法不可替换,Agent 的行为边界就会被框架作者固定。

架构重点:Loop 是 Harness 的调度器

Tool 决定 Agent 能做什么,Context 决定 Agent 看到什么,Model 决定 Agent 如何推理,而 Loop 决定 Agent 下一步怎样行动。因此 Loop 必须像 Model、Tool、Memory 一样成为可配置、可测试、可替换的一等组件。

2.4.2 三种典型 Loop 的适用边界

Loop 类型控制方式适用场景主要风险
ReAct LoopReason → Act → Observe 循环探索式任务、交互式排障、代码库调查步数膨胀、上下文污染、终止条件不稳定
Plan-and-Execute Loop先规划任务,再按计划执行与验证长任务、交付型 Coding Agent、迁移重构初始计划错误会系统性放大,需要中途重规划
CodeAct Loop模型生成代码,由沙盒执行,再回传结果数据分析、批处理、结构化文件操作沙盒与权限必须严格,避免代码副作用外泄
flowchart TB
    Input["用户任务"] --> Router{"Loop Router<br/>任务类型 + 风险等级 + 预算"}

    Router -->|"开放探索"| ReAct["ReAct Loop<br/>逐步观察与行动"]
    Router -->|"长任务交付"| Plan["计划-执行 Loop<br/>计划 / 执行 / 验证"]
    Router -->|"结构化计算"| CodeAct["CodeAct Loop<br/>生成代码 / 沙盒执行 / 汇总"]

    ReAct --> Trace["统一 Trajectory"]
    Plan --> Trace
    CodeAct --> Trace

    Trace --> Eval["Replay / Fork / Eval"]

    style Router fill:#fff3e0,stroke:#f57c00
    style Trace fill:#e1f5fe,stroke:#0288d1,stroke-width:2px

最佳实践:Loop Router 不等于让模型自由选 Loop

Loop 的选择应由 Harness 的确定性策略主导:任务风险、工具副作用、预算、是否需要审批、是否存在可验证输出。LLM 可以提供分类建议,但最终路由应由规则和策略裁决。

2.4.3 Loop 插件的最小接口

一个可替换的 Loop 插件,至少需要暴露四类接口:

"""
Loop 插件的最小抽象:让 Agent 的控制算法可替换、可观测、可评测。
"""
from dataclasses import dataclass
from typing import Protocol, Literal
 
 
@dataclass
class LoopDecision:
    action: Literal["call_model", "call_tool", "spawn_subagent", "ask_approval", "finish", "fail"]
    reason: str
    payload: dict
 
 
class AgentLoop(Protocol):
    name: str
    version: str
 
    def initialize(self, task: "Task", ctx: "AgentContext") -> "LoopState":
        """初始化 Loop 状态,生成第一个 checkpoint。"""
        ...
 
    def decide(self, state: "LoopState", observation: "Observation") -> LoopDecision:
        """根据当前状态和环境观察,决定下一步动作。"""
        ...
 
    def apply(self, state: "LoopState", event: "LoopEvent") -> "LoopState":
        """将模型响应、工具结果、审批结果等事件写回状态。"""
        ...
 
    def should_stop(self, state: "LoopState") -> bool:
        """由确定性条件判断是否终止。"""
        ...

这个接口背后的关键不是代码形式,而是职责边界:

  • Loop 插件负责决策节奏,不直接绕过权限系统
  • Tool 调用必须经过 Tool Registry
  • Context 构造必须经过 Context Assembler
  • 状态变化必须写入 Session / Checkpoint
  • 每个 decision 必须进入 Trajectory,便于 Replay 和 Eval

2.4.4 插件化带来的复杂度成本

工程避坑:"万物皆插件"不等于"万物都应该插件化"

当 Model、Loop、Memory、Context、Tool、Sandbox、Orchestrator 全部可替换时,系统表达力会显著提升,但故障定位也会变难。一个 Bad Case 可能来自模型输出、Prompt、上下文压缩、Loop 决策、工具错误、记忆污染、沙盒差异,或者插件之间的交互。

因此,完全插件化的 Harness 必须同时建设三件事:

基础设施作用没有它的后果
可观测性(Observability)记录每个插件的输入、输出、耗时、状态变化只能看到最终失败,看不到失败传播路径
重放 / 分叉(Replay / Fork)从任意 checkpoint 重放或分叉执行无法复现线上 Bad Case,也无法比较修复效果
评估(Evaluation)对 Model / Loop / Context / Tool 组合做回归评测插件替换靠感觉,版本升级不可控

核心观点

插件化 Harness 的成败不取决于插件数量,而取决于它能否把插件组合后的行为变成可观测、可复现、可评测的工程事实。


3. 测试与评测脚手架(Evaluation Harness)

3.1 脚手架是循环工程的运行载体

概念解析:脚手架工程与循环工程的关系

在 循环工程 中,我们定义了 LLM 应用的”评估-反馈-修正”闭环——度量(Metrics)、评估(Eval)、反馈(Feedback)、优化(Optimize)。但这些闭环不是”自动运行”的——它们需要一个运行时环境来承载。 这个运行时环境就是评测脚手架(Evaluation Harness)。

如果说循环工程定义了评测什么和优化逻辑,那么评测脚手架定义了如何运行这些评测——包括测试集的批量并发执行、沙盒环境隔离、指标收集与报表生成。

flowchart TB
    subgraph LoopEngineering["循环工程:定义评测逻辑"]
        L1["评测指标定义"]
        L2["测试用例构造"]
        L3["评分器实现"]
        L4["优化策略"]
    end

    subgraph EvalHarness["评测脚手架:承载运行时"]
        E1["测试集批量运行<br/>(并发调度 + 速率限制)"]
        E2["沙盒环境隔离<br/>(每次评测独立上下文)"]
        E3["指标收集与聚合<br/>(结构化输出 + 统计计算)"]
        E4["报表生成<br/>(Markdown / JSON / 可视化)"]
        E5["CI/CD 集成<br/>(Git Hook / GitHub Action)"]
    end

    LoopEngineering -->|"定义"| EvalHarness
    EvalHarness -->|"反馈结果"| LoopEngineering

    style LoopEngineering fill:#fce4ec,stroke:#c62828
    style EvalHarness fill:#fff3e0,stroke:#f57c00

3.2 评测脚手架的核心能力

3.2.1 批量并发运行测试集

架构重点:评测的速度瓶颈与并行策略

一次 LLM Eval 通常包含 50-500 个测试用例,每个用例需要 1-5 秒的 LLM 推理时间。串行运行需要几分钟到几十分钟——这在开发迭代中是不可接受的。评测脚手架的第一个核心能力是:高效、安全的批量并发执行。

关键设计考量:

考量策略
API Rate Limit根据模型提供商的速率限制(如 Anthropic 的 RPM/TPM)设置并发上限。串行化请求时在脚手架层做排队,而非让 API 返回 429
测试用例隔离每个测试用例使用独立的对话上下文,确保用例之间不互相污染
错误处理单个用例的 LLM 调用超时或失败不应中断整个评测批次。收集错误信息,在评测结束后统一报告
进度反馈长时间运行的评测应有实时进度输出(已完成 N/500,预计剩余 X 分钟)
可复现性记录每次评测的完整参数(模型版本、Prompt 版本、temperature、测试集版本),确保结果可复现

3.2.2 沙盒环境隔离

工程避坑:评测不能污染生产环境

如果你的 Agent 评测涉及到写操作(创建文件、发送消息、修改数据库)——你绝对不能在评测时操作真实的生产资源。评测脚手架需要提供沙盒环境:一个与生产完全隔离的执行环境。

沙盒的常见实现方式:

  • Mock 层注入:在评测模式下,工具注册表自动将所有写操作替换为 Mock 实现(如 send_email() 变为 log_send_email_intent())
  • 临时资源池:为评测创建临时的数据库 / 文件系统 / API endpoint,评测结束后自动清理
  • Docker 容器隔离:在 CI 中,整个评测流程在 Docker 容器中运行,测试结束后销毁容器

3.2.3 指标收集与报表生成

架构思考:评测脚手架的输出——从"跑完了"到"可决策"

评测脚手架不只是”跑完所有测试”,它的核心价值在于将原始执行结果转化为可指导决策的结构化信息:

原始结果 → 脚手架汇聚层 → 结构化报告

原始结果:
  Test #001: score=0.85, latency=2.3s, tokens=1240
  Test #002: score=0.62, latency=3.1s, tokens=1850
  ...

汇聚层:
  - 总体得分: 平均 0.78, 中位数 0.82, P95=0.91
  - 耗时分析: 平均 2.4s, P95=4.1s, P99=8.7s
  - 失败用例: 12/200 (6%) — 其中 8 个因 JSON 解析失败, 4 个因超时
  - 与上一版本对比: 平均得分 +0.03 (↑3.8%), 但 JSON 解析失败率 +2%
  - 耗时回归: P99 延迟增加了 2.1s (可能因为新版 Prompt 更长)
  - 建议: JSON 解析失败率上升, 建议检查 Prompt 中 format 指令的变化

最佳实践:评测报表的"红绿灯"分级

不要给团队扔一堆数字。评测脚手架应该输出一个直觉化的”红绿灯”结论:

  • 🟢 绿灯:所有指标在可接受范围内,可以合并/发布
  • 🟡 黄灯:部分指标有退化但在容忍范围内,建议人工 review 后决定
  • 🔴 红灯:关键指标显著退化,阻止合并/发布

阈值的设定是一个需要持续校准的过程——详见 循环工程中的数据飞轮。

3.2.4 CI/CD 集成

flowchart LR
    subgraph CI["CI Pipeline"]
        Push["代码 Push"] --> Build["构建"]
        Build --> UnitTest["单元测试<br/>(确定性组件)"]
        UnitTest --> EvalHarness["评测脚手架<br/>(LLM Eval)"]
        EvalHarness --> Report{报表评估}
        Report -->|"🟢 绿灯"| Merge["允许合并"]
        Report -->|"🟡 黄灯"| Warn["告警 + 人工 Review"]
        Report -->|"🔴 红灯"| Block["阻止合并"]
    end

    style EvalHarness fill:#fff3e0,stroke:#f57c00,stroke-width:2px

架构重点:将 Eval 嵌入 CI——LLM 项目的"回归测试"

传统软件的回归测试确保代码改动不会破坏已有功能。LLM 项目的回归测试需要确保 Prompt 改动、模型升级、上下文策略变更不会导致整体质量退化。评测脚手架是 LLM 项目 CI 流程中不可缺失的一环。

但需要注意:LLM Eval 比传统单元测试慢 100-1000 倍,且需要消耗 API 费用。因此评测脚手架应支持:

  • 分层运行:PR 提交时只跑快速 Eval(50 个精选用例,< 2 分钟),每日构建时跑全量 Eval(500 用例)
  • 智能用例选择:根据改动范围只运行相关的测试子集(改动了 SQL 生成 Prompt → 只跑 SQL 相关用例)
  • 成本控制:设置单次评测的 Token 预算上限

4. 框架选型与演进:重型装甲还是轻量级拼装?

4.1 当前编排工具栈全景

架构思考:市面上的编排方案概览

flowchart TB
    subgraph Spectrum["编排框架光谱:从轻量到重型"]
        direction LR
        A["手搓脚手架<br/>原生 SDK + 自建状态机<br/>~500 行 Python"]
        B["专项工具库拼装<br/>Pydantic + Tenacity + structlog<br/>各组件独立选型"]
        C["轻量编排框架<br/>Vercel AI SDK / LlamaIndex<br/>(底层解耦,只引入编排逻辑)"]
        D["状态机框架<br/>LangGraph<br/>(专注状态流转,不绑架其他层)"]
        E["重型全栈框架<br/>LangChain (早期) / CrewAI<br/>(提供从 Prompt 到部署的全套抽象)"]
    end

    A -->|"复杂度增长"| B -->|"抽象需求增长"| C -->|"状态管理需求"| D -->|"多 Agent 协同需求"| E

    style A fill:#e8f5e9,stroke:#2e7d32
    style B fill:#c8e6c9,stroke:#388e3c
    style C fill:#fff9c4,stroke:#f57f17
    style D fill:#ffe0b2,stroke:#e65100
    style E fill:#ffcdd2,stroke:#c62828
方案代码量(你的)灵活性学习曲线适合场景
手搓脚手架500-1500 行最高(完全控制)高(需要理解所有底层机制)单一明确场景,团队有强工程能力
专项工具库300-800 行高(每个组件独立选型)中(需要了解各库的 API)已有清晰架构,需要引入最佳实践的库
轻量编排框架200-500 行中高(框架只做编排,不绑架细节)中需要快速开发但不想被框架束缚
状态机框架300-1000 行中(状态流转由框架管理)中高复杂多步 Agent,需要可视化和调试
重型全栈框架100-300 行(但调试成本极高)低(框架决定一切)看似低,实则高(遇到边界问题要调试框架源码)快速原型验证,不建议直接用于生产

4.2 抽象泄漏:早期 LangChain 的教训

工程避坑:框架的"抽象泄漏(Leaky Abstraction)"问题

2023 年的 LangChain 是大模型应用开发的事实标准。它的初衷是美好的:通过 Chain / Agent / Tool 等高级抽象,让开发者不需要关心底层 API 细节。但许多团队在生产环境撞了墙:

问题一:Prompt 模板的黑箱 LangChain 的 PromptTemplate 和 ChatPromptTemplate 在底层做了大量自动拼接逻辑。当 Prompt 在线上出现问题时,开发者无法快速定位”实际发送给模型的内容到底是什么”——因为他们看到的只是模板变量,真实 Prompt 被框架在运行时动态组装。

问题二:错误信息的吞噬 框架的多层抽象带来了深层调用栈。当 LLM API 返回一个错误时,原始错误信息被逐层包装、转换、吞并。开发者看到的是 ChainError: Agent stopped due to iteration limit——而不是”第 3 步工具调用超时,因为下游 API 返回了 503”。

问题三:版本升级的破坏性 LangChain 在 0.0.x 到 0.1.x 到 0.2.x 的版本迭代中,API 发生了多次不兼容变更。一个在 0.0.350 上完美运行的 Agent,升级到 0.1.0 后可能需要重写 30% 的代码。框架的进化速度超过了团队的适应速度。

核心观点

框架的价值 = 它为你节省的时间 - 你为理解它和绕过它的限制所花费的时间。 如果一个框架的抽象层厚到你看不见底层发生了什么,那么当底层出问题时,你将完全束手无策。这就是为什么 2024-2025 年行业出现了一个明显的趋势:从”信任框架”转向”理解底层后再选择性使用框架”。

4.3 2025-2026 年的趋势:轻量级编排与手搓回归

架构思考:行业正在向"最小可行抽象"收敛

经历了 LangChain 的”抽象过度”教训后,2025-2026 年的行业共识正在向中间地带收敛:

趋势一:LangGraph 的定位——只做状态管理,不绑架其他层

LangGraph 是 LangChain 团队从”过度抽象”中吸取教训后的产物。它不再试图提供从 Prompt 到部署的全套方案,而是只做一件事:用有向图(Graph)描述 Agent 的状态流转。Prompt 的组装、模型的调用、工具的注册——这些全部留给开发者自己控制。

LangChain(早期): "用我的方式做一切"
LangGraph(现在):  "我只管状态流转,其他的你自己来"
手搓(始终):      "我全都要自己来,但我理解每一行代码"

趋势二:Vercel AI SDK 的”薄封装”哲学

Vercel AI SDK 代表了另一种设计哲学:不对 LLM API 做过多抽象,只提供流式处理、中间件、工具调用的薄封装。 开发者始终能接触到原始的 API 参数,框架只负责处理”无聊但必要的工程细节”(如流式响应的解析、多 Provider 的统一接口)。

趋势三:MCP 协议——工具集成的标准化

MCP 协议 的出现解决了工具层的最核心痛点:每个 Agent 框架都有自己的工具定义格式,导致工具无法跨框架复用。MCP 提供了一个标准化的协议——无论你的 Agent 使用什么框架,工具都遵循相同的描述格式和调用约定。这是脚手架层标准化的重要一步。

4.4 架构师的选型建议

架构重点:渐进式引入,而非一次性选择

不要在项目的第一天就选好”我们要用哪个框架”。 框架选型应该跟随你对问题的理解深度而演进:

阶段推荐方案理由
原型验证(1-2 周)原生 SDK + 最少代码验证的是”这个场景 LLM 能否胜任”,而不是”框架好不好用”
MVP 开发(1-2 月)引入专项工具库(Pydantic + Tenacity + structlog)保留完全控制权,同时复用成熟的工程组件
生产打磨(2-6 月)评估轻量编排框架(LangGraph / Vercel AI SDK)在理解所有底层机制后,选择性引入框架来减少重复代码
规模化(6 月+)混合架构(核心链路手搓 + 非核心链路用框架)关键路径保持自主可控,非关键路径用框架加速

最佳实践:"兜底不使用框架也能跑"

一个健康的 Agent 架构应该满足一条简单的测试:如果你删掉框架,只用原生 SDK + 你的状态机代码,系统是否还能基本运行? 如果答案是”不能”——说明你的业务逻辑已经和框架深度耦合,换框架的成本将等同于重写。如果答案是”能”——说明框架只是一个加速器,你可以随时替换它。

这不是反框架的立场。这是让框架成为你的工具,而非你的主人。


5. 结语:脚手架是通向 Multi-Agent 的必经之路

5.1 脚手架工程的终极价值

核心观点

至此,“四大工程”的完整版图已经呈现:

如果说前三者决定了一个 Agent “有多聪明”,脚手架决定了一个 Agent “有多可靠”。在真实的用户场景中——可靠性永远比聪明更重要。 用户会容忍一个偶尔说”我不确定”的 Agent,但不会容忍一个”大部分时候很聪明,但偶尔会删掉数据库”的 Agent。

flowchart TB
    subgraph FourPillars["四大工程的协同关系"]
        direction TB

        P["🎯 提示词工程<br/><i>决定上限</i>"]
        C["🧠 上下文工程<br/><i>扩展记忆</i>"]
        L["🔄 循环工程<br/><i>持续优化</i>"]
        H["🏗️ 脚手架工程<br/><i>承载一切</i>"]

        P --> H
        C --> H
        L --> H
    end

    subgraph Outcomes["产出"]
        O1["从"易碎的魔法"<br/>→ 健壮的软件资产"]
        O2["从"能跑通 Demo"<br/>→ 能抗住线上压力"]
        O3["从"单人开发的小工具"<br/>→ 团队协作的工程系统"]
        O4["从"单 Agent"<br/>→ Multi-Agent 协同"]
    end

    H --> Outcomes

    style H fill:#fff3e0,stroke:#f57c00,stroke-width:3px
    style Outcomes fill:#e8f5e9,stroke:#2e7d32

5.2 通向 Multi-Agent:脚手架是不可绕过的地基

架构思考:为什么说脚手架是 Multi-Agent 的前提?

当你开始构建 Multi-Agent 系统(多个 Agent 协同完成一个任务——详见 Multi-Agent),脚手架工程的价值会从”重要”变为”不可或缺”:

  • 单 Agent 场景:你还可以用”一个 while 循环 + 一个 LLM 调用”勉强维护
  • Multi-Agent 场景:你需要 Agent 之间的消息路由、状态同步、任务分配、冲突解决、全局断路器——这些没有一个是不需要脚手架的

试图在没有脚手架的情况下构建 Multi-Agent 系统,就像试图在没有操作系统的情况下运行多个进程。 你会发现自己不是在”开发 Agent”,而是在”手动管理并发、通信和故障恢复”——而这些恰恰是脚手架工程的核心能力。

5.3 从”写代码”到”建系统”的认知跃迁

最后的思考

回顾整个”四大工程”的学习路径:

你从 重新认识 AI 开始,理解了 LLM 的本质是一个概率预测系统。然后通过 提示词工程 学会了如何与这个系统高效对话,通过 上下文工程 为它配备外部记忆,通过 循环工程 建立了质量保障体系。

现在,通过本文——脚手架工程——你应该意识到一个更深刻的认知跃迁:

构建大模型应用不仅仅是”调用 API”,而是在建造一个全新的软件架构范式。 在这个范式中,确定性代码(脚手架)和非确定性推理(LLM)必须深度协同。脚手架不是 LLM 的”仆人”——它是 LLM 的”运行容器”。就像操作系统不是应用程序的”仆人”,而是为应用程序提供内存管理、进程调度、文件系统、网络通信的基础设施。

从”写代码”到”建系统”——这是脚手架工程赋予你的终极视角。


本文要点回顾

  • 脚手架定位:Harness 是四大工程中的”粘合层”——它将 Prompt、Context、Loop 整合为可运行、可维护、可演进的系统。LLM 是引擎,脚手架是底盘。
  • 控制面与数据面:脚手架工程的核心思想是引入”控制面”(确定性路由、状态机、中间件)来管理”数据面”(非确定性的 LLM 推理)。
  • 路由与状态机:路由层将模糊输入映射到确定执行路径(三级策略:规则→语义→兜底)。状态机是 Agent 的”纪律层”——模型思考,状态机约束。
  • DAG 编排:当线性状态机不够用时,DAG 提供依赖关系的显式声明、并行执行和故障隔离。LLM 节点应像纯函数一样设计。
  • 中间件链:前置中间件负责安全、预算、缓存;后置中间件负责校验、安全、计费、质量评估。核心模式是责任链(Chain of Responsibility)。
  • 工具集成层:工具注册表将”定义”(给 LLM 看)与”实现”(实际执行)分离。超时 + 重试 + 熔断是工具层的安全网。错误信息应”写给模型看”。
  • 运行时内核视角:高级 Harness 不只是给 Agent 加工具,而是把 Loop、上下文策略、Session、Memory、Sandbox、Orchestration 都变成可组合的运行时组件。
  • 能力优先:通用 Agent 不应过早写死 Workflow,而应先定义能力、权限、预算、失败策略和可观测信号,再由 Loop 动态组合能力。
  • Loop 插件化:Loop 是 Agent 的控制算法,应像 Model 和 Tool 一样可替换、可观测、可评测;插件化必须配套可观测性、重放 / 分叉和评估体系。
  • 评测脚手架:是循环工程的运行载体——提供批量并发执行、沙盒隔离、指标收集、CI/CD 集成。将 Eval 嵌入 CI 是 LLM 项目的”回归测试”。
  • 框架选型:从原型到手搓到轻量框架的渐进式路线。避免”抽象泄漏”——始终满足”删掉框架也能跑”的底线。
  • Multi-Agent 前提:脚手架是构建 Multi-Agent 系统的不可绕过的基础设施。

全系列文章索引

  • 重新认识AI — LLM 的底层本质与 Agent 系统全景
  • 提示词工程 — 提示词作为接口协议的设计、评估与版本管理
  • 上下文工程 — RAG、MCP、记忆管理的完整链路
  • 循环工程 — Eval 体系、数据飞轮、LLM CI/CD 的工程实践
  • 脚手架工程(本文)— 大模型应用的编排与基础设施构建
  • MCP — AI 连接外部世界的标准化协议
  • Agent实践指南 — 从 Demo 到工业级的完整工程实战
  • Multi-Agent — 多 Agent 协同的架构设计与隔离策略