第四章 问题空间:从状态到路径的工程地图

核心命题:工程师在动手构建 Harness 之前,首先需要回答一个更基本的问题:这个问题长什么样? 问题空间理论提供了回答这个问题的精确语言——它将”待求解的问题”建模为一张有向图,将”求解过程”建模为在这张图上的路径搜索。理解这张图的结构,是系统化设计 Harness 的前提;设计不当的 Harness,往往源于对问题空间结构的错误假设。

与第一章的分工:第一章(1.3节)以直觉性语言描述了”Agent 在可能性空间中搜索”这一核心图景——约束如何将开放的搜索空间压缩到可行域,但未引入形式符号。本章将这一直觉形式化:正式引入 Newell-Simon 问题空间框架 P=S,O,s0,GP = \langle \mathcal{S}, \mathcal{O}, s_0, \mathcal{G} \rangle,从形式定义到状态表示的设计选择,从操作集的构造逻辑到搜索的可行性约束,最终将问题空间理论转化为 Harness 工程师的分析工具。

4.1 形式定义与四个组件的工程含义

问题空间的形式定义:

P=S,O,s0,GP = \langle \mathcal{S}, \mathcal{O}, s_0, \mathcal{G} \rangle

其中 S\mathcal{S}状态空间(所有可能的系统状态的集合),O\mathcal{O}操作集——其中每个 oOo \in \mathcal{O} 都是一个状态转换函数 o:SSo: \mathcal{S} \to \mathcal{S}s0Ss_0 \in \mathcal{S}初始状态GS\mathcal{G} \subseteq \mathcal{S}目标状态集合。Agent 的任务是找到一条从 s0s_0 出发、经由 O\mathcal{O} 中操作的有限序列,最终到达某个 gGg \in \mathcal{G} 的路径——即一个解。

这个定义看起来抽象,但每个组件都有直接的工程含义:

关于确定性假设的说明:此处的形式定义将每个操作 oOo \in \mathcal{O} 视为确定性转换函数——给定状态 ss 与操作 oo,总是到达同一个确定的新状态 s=o(s)s' = o(s)。这是一个简化假设:真实环境中,工具调用会失败、网络会超时、LLM 输出本身带有随机性,状态转换实为概率分布 P(ss,o)P(s' \mid s, o) 而非确定函数。本章保留 Newell-Simon 框架以清晰表达工程直觉;第五章 §5.5 将正式引入随机状态转换的概念,附录 A.5 提供马尔可夫决策过程(MDP)的严格对应模型。

状态空间 S\mathcal{S}:Agent 的”世界模型”。状态是 Agent 对当前系统快照的表示。设计状态表示时,最核心的工程决策是:哪些信息必须被纳入状态?哪些可以安全忽略? 过于稀疏的状态表示会导致 Agent 在信息不足时做出错误决策;过于稠密的状态表示则带来两个代价——Context 带宽消耗(第六章)和搜索空间的爆炸式增长。一个代码修复任务的状态,需要包含当前的失败测试列表和相关文件内容,但不需要包含项目的完整提交历史——后者对于当前决策没有增量信息价值,却会消耗大量 Context 空间。

操作集 O\mathcal{O}:Harness 的能力边界。操作是 Agent 能够执行的动作——每个操作接受一个状态,返回一个新状态。操作集的边界,就是 Harness 允许 Agent 进入的状态集合的边界。这是第十章(工具权限治理)的问题空间理论根基:提供 apply_patch 而非 raw shell exec,不只是权限控制的工程选择,而是在问题空间层面将 Agent 的可达状态限制在一个被精心设计过的子集中。

初始状态 s0s_0:任务的起点。初始状态的质量直接决定了 Agent 搜索的起点质量。一个准确的初始状态(包含正确的文件内容、依赖版本、测试结果)让 Agent 从合适的位置出发;一个过期的初始状态(包含已被修改的文件缓存)可能让 Agent 在错误的子空间中搜索数十步后才发现方向性错误。Context Engineering 的任务之一,就是确保 s0s_0 的新鲜度与准确性。

目标状态集 G\mathcal{G}:任务的终点。目标状态集的设计是 Harness 工程中最容易被忽视的组件。大多数教程讨论如何让 Agent”更好地”完成任务,却很少讨论”完成”本身如何被定义。当 G\mathcal{G} 定义得足够精确(如”所有测试通过”),验证是自动化的;当 G\mathcal{G} 模糊(如”代码质量提升”),验证需要语义判断——这个判断必须由人类或语义型传感器承担,是第十章 Hook 设计的核心挑战之一。

4.2 状态表示的设计选择

状态不是客观存在,而是工程选择。 同一个物理系统可以被建模为截然不同的状态表示,每种表示都定义了不同的搜索结构。

以数据库迁移任务为例,存在两种截然不同的状态定义方式:

方案 A(粗粒度状态):状态 = {迁移脚本版本号, 目标表结构 Schema}。操作集规模小,搜索空间小,但状态之间的转换是”大步跳跃”,错误发生时难以定位。

方案 B(细粒度状态):状态 = {当前 Schema, 已执行的 SQL 列表, 受影响的表与行数, 回滚点快照}。操作集更丰富,搜索空间更大,但每个操作产生的状态变化更小、更可验证,错误信号更精确。

两种方案揭示了同一个概念事实:粒度选择同时影响错误的可定位性与搜索空间的可管理性——不是偏好问题,而是对问题”自然分解单元”的寻找。粗到错误难以定位,细到搜索空间膨胀——状态表示因此在完整性、新鲜度、Context 密度三个相互制约的维度上展开权衡。如何在这三者之间做出最优设计决策,是第八章(Context Engineering)和第九章(Planning)的工程主题。

4.3 搜索的本质:在图上寻路

将问题空间可视化为一张有向图:节点是状态,边是操作(从一个状态到另一个状态的转换)。求解问题可类比为在这张图中找一条从 s0s_0 到某个 gGg \in \mathcal{G} 的路径。

这个视角揭示了若干工程上有直接意义的性质:

搜索的代价是路径的长度。路径上的每一个节点对应一次 Agent 的行动——工具调用、状态更新、结果验证。路径越长,总 token 消耗越大,总时延越高。Harness 的目标之一,是将 Agent 引导到较短的有效路径上,而非任意一条能到达目标的路径。

搜索的可行性依赖于图的连通性。如果初始状态 s0s_0 与目标集 G\mathcal{G} 在操作集 O\mathcal{O} 定义的图中不连通——即无论 Agent 如何组合操作,都无法从 s0s_0 到达任何目标状态——则任务本质上不可行,无论 Agent 能力多强都无济于事。这类不可行性通常来自两个来源:操作集被过度约束(权限设计过于保守,关键操作被封锁);或初始状态被错误建模(任务实际上需要的前提条件没有满足)。

启发式函数是搜索的加速器。在大型状态空间中,无引导的搜索效率极低。启发式函数 h(s)h(s) 估计从当前状态 ss 到目标状态的距离,使搜索可以优先探索”更接近目标”的分支。在 Harness 工程中,这对应于 Plan 的规划能力——一个好的 Plan 相当于提供了较高质量的启发式,引导 Agent 优先尝试高概率的解路径,而非均匀随机地探索操作空间。

分支因子决定搜索的指数复杂度。在每个状态下,操作集 O\mathcal{O} 中有多少个操作可以被合法应用,这个数量称为该状态的分支因子 bb。若平均分支因子为 bb、解路径长度为 dd,则需要探索的节点数在最坏情况下是 O(bd)O(b^d)——随路径长度指数增长。这是第三章中”长链推理可类比为 NP 难问题”命题的几何直觉:一旦解路径稍长,即便分支因子适中,暴力搜索也会迅速变得不可负担。

O(bd)O(b^d) 的形态可以直接读出 Harness 的两条干预路径:要么压缩 bb(在每一步剪枝可选操作),要么压缩 dd(缩短解路径所需的步数)。两者乘积关系决定了——单一维度的压缩往往不够,必须协同:

搜索成本O(bd)Harness 的第一要务:同时压缩 b 和 d\text{搜索成本} \sim O(b^d) \quad \Rightarrow \quad \text{Harness 的第一要务:同时压缩 } b \text{ 和 } d

Harness 对搜索复杂度的干预作用在下图中清晰可见:

## Harness 对搜索复杂度的干预作用

### 无 Harness 约束
*分支因子 b 大,路径深度 d 大*
                s₀
               /|\ \
              / | \ \
             /  |  \ \
            ●   ●   ●  ●        ← 第 1 层(b 个分支)
           /\  /\  /\ /\
          ●  ●●  ●●  ●●  ●      ← 第 2 层(b² 个节点)
         /\ /\ /\ /\ /\ /\ /\
        ●  ●● ● ●●  ●● ● ●●    ← 第 3 层(b³ 个节点)
        ·  ·  ·  ·  ·  ·  ·
        ·  ·  ·  ·  ·  ·  ·
           …指数级节点…

搜索成本 ≈ O(bᵈ) → **不可行**

---

### Harness 约束后
*b 压缩,d 压缩,搜索可行*
                s₀
               / \
              /   \
             ●     ●             ← 第 1 层(b 压缩)
            / \   / \
           ●   ● ╌╌╌╌╌╌╌ (剪枝)  ← 第 2 层
                \
                 ●              ← 第 3 层(d 压缩)
                  \
                   G            ← 目标状态(较短路径可达)

搜索成本 ≈ O(bᵈ) → **b↓ d↓ → 可行**

---

### 图例

| 符号 | 含义 |
|------|------|
| `s₀` | 初始状态 |
| `G`  | 目标状态 |
| `●`  | Harness 约束后的活跃节点 |
| `╌╌` | 被剪枝的节点(操作集约束或 Plan 启发式排除)|
| `·`  | 继续向下扩展的节点(无约束时指数增长)|
| `b`  | 分支因子(每个状态下可应用的操作数) |
| `d`  | 解路径深度(从 s₀ 到 G 的步数)|

4.4 操作集的边界:Harness 的能力外壁

操作集 O\mathcal{O} 不是被发现的,而是被设计出来的——设计什么操作”在集合内”,就等于设计了 Agent 可以到达哪些状态。操作集越宽,搜索空间越大、灵活性越高,但分支因子 bb 同步膨胀;操作集越窄,分支因子受抑、意外副作用的暴露面越小,但目标 G\mathcal{G} 在图中变得不可达的风险随之上升。在问题空间层面,这一设计决策的结构意义优先于具体的操作枚举;白名单与黑名单的工程权衡、最小操作集原则的实践方法与可达性验证,在第十章(Tool 与权限治理)中展开。

4.5 目标状态的模糊性:真正的工程难题

在教科书案例中,目标状态是精确定义的——“解方程 x=5x = 5“的目标状态是 {x=5}\{x=5\}。但在实际 Agent 任务中,目标状态的定义本身往往是最困难的工程问题。

三类目标定义及其工程代价

可形式化验证的目标(测试通过、格式合规、约束满足):最理想的情况。目标集 G\mathcal{G} 可以被自动验证,终止条件明确,Hook 的 Stop 节点可以自动化。Q/T/C 中的质量维度是可度量的。

可语义评估但不可形式化的目标(代码可读性提升、解释清晰度):需要语义型传感器介入——LLM-as-judge 或人工审核。G\mathcal{G} 存在,但边界模糊,不同评估者可能给出不同判断。这类目标需要在 System Prompt 中进行更精确的语言约束,将模糊的意图转化为尽可能可操作的标准。

目标本身尚未确定的任务(探索性研究、开放性创作):G\mathcal{G} 在任务开始时是空集,需要人类在过程中逐步确认哪些状态是”足够好的”。这类任务是 Human on the Loop 参与度最高的场景——人类不只是监督者,而是实时的目标定义者。

目标模糊性的工程对策

当目标无法被完全形式化时,可将其分解为若干可验证的必要子条件(Necessary Conditions)与若干软性偏好条件(Preferences)。必要子条件用计算型 Hook 验证——成本低、确定性高,可作为终止判据;偏好条件用语义型评估——成本高、概率性,仅作为质量加权而非终止门。这一分解将”模糊目标”转化为”精确约束 + 软性目标”的工程组合,是 Harness 中目标建模的标准实践。

4.6 问题空间的可分解性

许多复杂任务的问题空间具有可分解性——整体状态空间可以被划分为若干相对独立的子空间,各子空间可以分别搜索,再将子解组合为全局解。这个性质直接决定了 Plan(第九章)和 Multi-Agent(第十四章)的可行性:

可分解的前提:子问题之间的状态耦合度足够低——子问题 A 的求解不会大幅改变子问题 B 的状态空间结构。如果 A 和 B 高度耦合,强行分解会导致子解在组合时产生冲突,此时分解的协调成本超过了分解带来的搜索效率收益。

分解粒度的判断标准:一个可操作的经验规则——如果子问题的结果可以被简洁的结构化消息表达,则分解是合适的;如果 Agent B 需要 Agent A 的完整内部搜索路径才能正常工作,则分解粒度过细,或子问题间耦合度过高。

状态一致性是分解的前提:分解后各子 Agent 维护各自的局部状态视图,但整体系统必须在关键节点进行状态同步,防止不同子 Agent 的操作对共享状态产生相互矛盾的修改。这是第十四章讨论 Multi-Agent 一致性问题的理论根基。

章末案例剖析

同一个代码库重构任务,在两种问题空间建模下产生了截然不同的结果——不是因为 Agent 能力不同,而是因为 P=S,O,s0,GP = \langle \mathcal{S}, \mathcal{O}, s_0, \mathcal{G} \rangle 的四个组件被以不同方式设计,导致搜索问题的结构根本不同。

任务设定:将一个 Python 后端项目中的 UserManager 类重命名为 AccountService。代码库共 50 个文件,其中 12 个文件包含 UserManager 的引用(类定义 1 处、import 语句 8 处、实例化调用 14 处)。目标:所有引用完成替换,全部测试通过。这是一个边界清晰、答案可验证的重构任务——目标 G\mathcal{G} 属于第 4.5 节的第一类(可形式化验证),终止条件为”所有单元测试通过”。


建模 A:全量代码库快照 × 任意文件修改

s0s_0:50 个文件的完整内容(每文件平均 300 行,约 3 token/行)= 约 45,000 tokens 的状态表示。

O\mathcal{O}:任意文件写入——Agent 可以修改 50 个文件中任意一个的任意内容。每个文件的可能修改方式在实践中接近无限;即便保守估计每文件 20 种有意义的修改,整体操作集规模 = 50 × 20 = ~1,000 个有效操作,分支因子 b1,000b \approx 1,000

G\mathcal{G}:未显式定义(只告诉 Agent”完成重命名”)。Agent 需要自行推断终止条件。

执行过程

第一轮,Agent 尝试加载 s0s_0——45,000 tokens 的状态表示已超过大多数模型的 Context 窗口上限(或即便未超,已消耗绝大多数 Context 预算,留给推理和输出的空间极小)。Agent 被迫截断输入;截断后的状态不完整,Agent 只看到部分文件内容,无法知道哪些文件包含 UserManager 引用。

即便跨越了 Context 限制,第一步操作后新状态仍然是 50 个文件的完整内容——下一轮又需要重新加载。搜索复杂度 O(bd)=O(100010)O(b^d) = O(1000^{10}) 是数学上的必然结论:这不是”模型不够聪明”,而是在这个问题空间定义下,任何搜索策略都面临指数级代价。

结果:Agent 在第一轮耗尽 Context 预算,任务未完成。失败不来自 Agent 的推理能力,而来自问题空间建模使搜索从一开始就处于不可行的量级。


建模 B:精简状态 × 有界操作集

重新设计 PP 的四个组件:

s0s_0(精简初始状态,约 800 tokens):

{
  "task": "rename UserManager → AccountService",
  "references": null,          # 待填充:grep 后更新
  "interface_def": "<UserManager 的类签名与公开方法,约 30 行>",
  "test_results": "12 passed, 0 failed",
  "progress": {"renamed": [], "updated": [], "remaining": "TBD"}
}

O\mathcal{O}(6 个有界操作):

  • grep_references(name) → 返回包含该名称的文件列表,更新 references 字段
  • rename_class(file, old, new) → 重命名类定义本身
  • update_imports(file, old, new) → 更新 import 语句
  • update_calls(file, old, new) → 更新实例化与调用点
  • run_tests() → 返回当前测试通过/失败列表,更新 test_results
  • commit(message) → 提交当前变更

G\mathcal{G}test_results.failed == [](可自动验证)。

搜索结构:分支因子 bb:在任意状态下,通常只有 1-3 个操作是语义上合理的下一步(grep 完成后,下一步显然是处理列表中的第一个文件)。有效分支因子 beff3b_{\text{eff}} \approx 3。路径深度 dd:1(grep)+ 12(逐文件更新)+ 1(运行测试)+ 1(提交)= 15 步。搜索复杂度 O(315)14,000,000O(3^{15}) \approx 14,000,000——理论上仍大,但 Plan 提供了近乎确定性的步骤序列,实际有效搜索节点数 < 30。

执行过程

第一步,Agent 调用 grep_references("UserManager"),状态更新为 12 个文件的路径列表(约 80 tokens),progress 中 remaining 被填充。此后每步操作将一个文件从 remaining 移至 updated,状态大小始终保持在 ~800 tokens。第 14 步调用 run_tests(),得到”12 passed, 0 failed”,G\mathcal{G} 满足,Stop Hook 触发终止。


两种建模的定量对比

维度建模 A建模 B
初始状态大小 s0\|s_0\|~45,000 tokens~800 tokens
操作集规模 O\|\mathcal{O}\|~1,000(实际无界)6
有效分支因子 beffb_{\text{eff}}~1,000~3
解路径深度 dd~10(理论下界,未实现)~15(实际)
搜索复杂度O(100010)1030O(1000^{10}) \approx 10^{30},不可行O(315)O(3^{15}),Plan 引导后 < 30 节点
Q(任务完成率)0%(Context 第一轮耗尽)~95%(测试通过验证)
T(完成时间)失败,无法计量~4 分钟
C(总 token 消耗)第一轮即超预算~12,000 tokens
目标 G\mathcal{G} 的可验证性未定义自动验证(测试通过)

两种建模的核心差异:工程师的设计决策

建模 A 和 B 的根本差别不在于 Agent,而在于 Harness 工程师做了什么:

状态设计(§4.2):建模 A 将”世界的完整快照”作为状态,将完整性置于首位;建模 B 识别了当前步骤的决策相关信息(progress + references + test_results),主动丢弃与当前决策无关的内容(其他 38 个文件的内容)。状态不是越完整越好——完整性与 Context 密度之间的权衡,是工程判断,不是技术限制。

操作集设计(§4.4):建模 A 将”任意文件写入”视为操作,实际上没有设计操作集——只是把能力暴露给了 Agent;建模 B 的 6 个操作是针对这个具体任务类型显式设计的,每个操作的粒度恰好对应任务的一个原子步骤。操作集的设计是 Harness 的能力边界设计,而非工具清单的枚举。

目标定义(§4.5):建模 A 的目标是隐式的自然语言意图;建模 B 将目标形式化为可自动验证的条件(run_tests() 返回零失败),使 Stop Hook 可以确定性地判断终止——不需要 LLM-as-judge 的语义评估,也不依赖人工判断。

这三个决策都由 Harness 工程师在 Agent 运行之前做出。问题空间的结构是工程的产物,而非任务的自然属性——同一个重构任务,可以被建模为不可行问题,也可以被建模为十五步确定性流程。搜索失败之前,往往先发生了建模失败。