脚手架工程(Harness Engineering):大模型应用的编排与基础设施构建
本文面向读者
前置阅读
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 是否也能成为可替换、可组合、可观测的组件。
这两种抽象的差别非常大:
| 抽象层级 | 典型问题 | 工程对象 |
|---|---|---|
| 工具 / MCP | Agent 能做什么? | 外部能力与系统调用 |
| 技能(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 填充每个状态内的”智能行为”。
这个范式有三个关键推论:
-
状态转移不由 LLM 决定。 状态机中的
EXECUTING → VALIDATING → RETRYING → COMPLETED这类流转是由代码明确定义的,LLM 的职责仅限于在单个状态内部进行推理(如”校验阶段:这个 SQL 查询结果是否符合预期?”)。 -
状态是可持久化、可恢复的最小单元。 当 Agent 在
EXECUTING状态崩溃时,检查点机制可以将其恢复到该状态的重入口——而非从INIT重新开始。 -
每个状态都有明确的”合法操作白名单”。 在
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最佳实践:中间件的排序策略
中间件的执行顺序至关重要。一个推荐的前置中间件排序:
- 缓存检查器(最先执行——如果命中,后续全部跳过,节省最大成本)
- Token 预算检查器(在准备调用前做最后的预算核算)
- 敏感词过滤器(在数据离开服务器前的最后一道防线)
- 请求日志记录器(记录最终发送的请求内容,用于后续审计)
后置中间件推荐反向排序(后进先出),确保越底层的处理越靠近原始输出。
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 Loop | Reason → 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 脚手架工程的终极价值
核心观点
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 系统的不可绕过的基础设施。
全系列文章索引