第四章 问题空间:从状态到路径的工程地图
核心命题:工程师在动手构建 Harness 之前,首先需要回答一个更基本的问题:这个问题长什么样? 问题空间理论提供了回答这个问题的精确语言——它将”待求解的问题”建模为一张有向图,将”求解过程”建模为在这张图上的路径搜索。理解这张图的结构,是系统化设计 Harness 的前提;设计不当的 Harness,往往源于对问题空间结构的错误假设。
与第一章的分工:第一章(1.3节)以直觉性语言描述了”Agent 在可能性空间中搜索”这一核心图景——约束如何将开放的搜索空间压缩到可行域,但未引入形式符号。本章将这一直觉形式化:正式引入 Newell-Simon 问题空间框架 ,从形式定义到状态表示的设计选择,从操作集的构造逻辑到搜索的可行性约束,最终将问题空间理论转化为 Harness 工程师的分析工具。
4.1 形式定义与四个组件的工程含义
问题空间的形式定义:
其中 是状态空间(所有可能的系统状态的集合), 是操作集——其中每个 都是一个状态转换函数 , 是初始状态, 是目标状态集合。Agent 的任务是找到一条从 出发、经由 中操作的有限序列,最终到达某个 的路径——即一个解。
这个定义看起来抽象,但每个组件都有直接的工程含义:
关于确定性假设的说明:此处的形式定义将每个操作 视为确定性转换函数——给定状态 与操作 ,总是到达同一个确定的新状态 。这是一个简化假设:真实环境中,工具调用会失败、网络会超时、LLM 输出本身带有随机性,状态转换实为概率分布 而非确定函数。本章保留 Newell-Simon 框架以清晰表达工程直觉;第五章 §5.5 将正式引入随机状态转换的概念,附录 A.5 提供马尔可夫决策过程(MDP)的严格对应模型。
状态空间 :Agent 的”世界模型”。状态是 Agent 对当前系统快照的表示。设计状态表示时,最核心的工程决策是:哪些信息必须被纳入状态?哪些可以安全忽略? 过于稀疏的状态表示会导致 Agent 在信息不足时做出错误决策;过于稠密的状态表示则带来两个代价——Context 带宽消耗(第六章)和搜索空间的爆炸式增长。一个代码修复任务的状态,需要包含当前的失败测试列表和相关文件内容,但不需要包含项目的完整提交历史——后者对于当前决策没有增量信息价值,却会消耗大量 Context 空间。
操作集 :Harness 的能力边界。操作是 Agent 能够执行的动作——每个操作接受一个状态,返回一个新状态。操作集的边界,就是 Harness 允许 Agent 进入的状态集合的边界。这是第十章(工具权限治理)的问题空间理论根基:提供 apply_patch 而非 raw shell exec,不只是权限控制的工程选择,而是在问题空间层面将 Agent 的可达状态限制在一个被精心设计过的子集中。
初始状态 :任务的起点。初始状态的质量直接决定了 Agent 搜索的起点质量。一个准确的初始状态(包含正确的文件内容、依赖版本、测试结果)让 Agent 从合适的位置出发;一个过期的初始状态(包含已被修改的文件缓存)可能让 Agent 在错误的子空间中搜索数十步后才发现方向性错误。Context Engineering 的任务之一,就是确保 的新鲜度与准确性。
目标状态集 :任务的终点。目标状态集的设计是 Harness 工程中最容易被忽视的组件。大多数教程讨论如何让 Agent”更好地”完成任务,却很少讨论”完成”本身如何被定义。当 定义得足够精确(如”所有测试通过”),验证是自动化的;当 模糊(如”代码质量提升”),验证需要语义判断——这个判断必须由人类或语义型传感器承担,是第十章 Hook 设计的核心挑战之一。
4.2 状态表示的设计选择
状态不是客观存在,而是工程选择。 同一个物理系统可以被建模为截然不同的状态表示,每种表示都定义了不同的搜索结构。
以数据库迁移任务为例,存在两种截然不同的状态定义方式:
方案 A(粗粒度状态):状态 = {迁移脚本版本号, 目标表结构 Schema}。操作集规模小,搜索空间小,但状态之间的转换是”大步跳跃”,错误发生时难以定位。
方案 B(细粒度状态):状态 = {当前 Schema, 已执行的 SQL 列表, 受影响的表与行数, 回滚点快照}。操作集更丰富,搜索空间更大,但每个操作产生的状态变化更小、更可验证,错误信号更精确。
两种方案揭示了同一个概念事实:粒度选择同时影响错误的可定位性与搜索空间的可管理性——不是偏好问题,而是对问题”自然分解单元”的寻找。粗到错误难以定位,细到搜索空间膨胀——状态表示因此在完整性、新鲜度、Context 密度三个相互制约的维度上展开权衡。如何在这三者之间做出最优设计决策,是第八章(Context Engineering)和第九章(Planning)的工程主题。
4.3 搜索的本质:在图上寻路
将问题空间可视化为一张有向图:节点是状态,边是操作(从一个状态到另一个状态的转换)。求解问题可类比为在这张图中找一条从 到某个 的路径。
这个视角揭示了若干工程上有直接意义的性质:
搜索的代价是路径的长度。路径上的每一个节点对应一次 Agent 的行动——工具调用、状态更新、结果验证。路径越长,总 token 消耗越大,总时延越高。Harness 的目标之一,是将 Agent 引导到较短的有效路径上,而非任意一条能到达目标的路径。
搜索的可行性依赖于图的连通性。如果初始状态 与目标集 在操作集 定义的图中不连通——即无论 Agent 如何组合操作,都无法从 到达任何目标状态——则任务本质上不可行,无论 Agent 能力多强都无济于事。这类不可行性通常来自两个来源:操作集被过度约束(权限设计过于保守,关键操作被封锁);或初始状态被错误建模(任务实际上需要的前提条件没有满足)。
启发式函数是搜索的加速器。在大型状态空间中,无引导的搜索效率极低。启发式函数 估计从当前状态 到目标状态的距离,使搜索可以优先探索”更接近目标”的分支。在 Harness 工程中,这对应于 Plan 的规划能力——一个好的 Plan 相当于提供了较高质量的启发式,引导 Agent 优先尝试高概率的解路径,而非均匀随机地探索操作空间。
分支因子决定搜索的指数复杂度。在每个状态下,操作集 中有多少个操作可以被合法应用,这个数量称为该状态的分支因子 。若平均分支因子为 、解路径长度为 ,则需要探索的节点数在最坏情况下是 ——随路径长度指数增长。这是第三章中”长链推理可类比为 NP 难问题”命题的几何直觉:一旦解路径稍长,即便分支因子适中,暴力搜索也会迅速变得不可负担。
由 的形态可以直接读出 Harness 的两条干预路径:要么压缩 (在每一步剪枝可选操作),要么压缩 (缩短解路径所需的步数)。两者乘积关系决定了——单一维度的压缩往往不够,必须协同:
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 的能力外壁
操作集 不是被发现的,而是被设计出来的——设计什么操作”在集合内”,就等于设计了 Agent 可以到达哪些状态。操作集越宽,搜索空间越大、灵活性越高,但分支因子 同步膨胀;操作集越窄,分支因子受抑、意外副作用的暴露面越小,但目标 在图中变得不可达的风险随之上升。在问题空间层面,这一设计决策的结构意义优先于具体的操作枚举;白名单与黑名单的工程权衡、最小操作集原则的实践方法与可达性验证,在第十章(Tool 与权限治理)中展开。
4.5 目标状态的模糊性:真正的工程难题
在教科书案例中,目标状态是精确定义的——“解方程 “的目标状态是 。但在实际 Agent 任务中,目标状态的定义本身往往是最困难的工程问题。
三类目标定义及其工程代价:
可形式化验证的目标(测试通过、格式合规、约束满足):最理想的情况。目标集 可以被自动验证,终止条件明确,Hook 的 Stop 节点可以自动化。Q/T/C 中的质量维度是可度量的。
可语义评估但不可形式化的目标(代码可读性提升、解释清晰度):需要语义型传感器介入——LLM-as-judge 或人工审核。 存在,但边界模糊,不同评估者可能给出不同判断。这类目标需要在 System Prompt 中进行更精确的语言约束,将模糊的意图转化为尽可能可操作的标准。
目标本身尚未确定的任务(探索性研究、开放性创作): 在任务开始时是空集,需要人类在过程中逐步确认哪些状态是”足够好的”。这类任务是 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 能力不同,而是因为 的四个组件被以不同方式设计,导致搜索问题的结构根本不同。
任务设定:将一个 Python 后端项目中的 UserManager 类重命名为 AccountService。代码库共 50 个文件,其中 12 个文件包含 UserManager 的引用(类定义 1 处、import 语句 8 处、实例化调用 14 处)。目标:所有引用完成替换,全部测试通过。这是一个边界清晰、答案可验证的重构任务——目标 属于第 4.5 节的第一类(可形式化验证),终止条件为”所有单元测试通过”。
建模 A:全量代码库快照 × 任意文件修改
:50 个文件的完整内容(每文件平均 300 行,约 3 token/行)= 约 45,000 tokens 的状态表示。
:任意文件写入——Agent 可以修改 50 个文件中任意一个的任意内容。每个文件的可能修改方式在实践中接近无限;即便保守估计每文件 20 种有意义的修改,整体操作集规模 = 50 × 20 = ~1,000 个有效操作,分支因子 。
:未显式定义(只告诉 Agent”完成重命名”)。Agent 需要自行推断终止条件。
执行过程:
第一轮,Agent 尝试加载 ——45,000 tokens 的状态表示已超过大多数模型的 Context 窗口上限(或即便未超,已消耗绝大多数 Context 预算,留给推理和输出的空间极小)。Agent 被迫截断输入;截断后的状态不完整,Agent 只看到部分文件内容,无法知道哪些文件包含 UserManager 引用。
即便跨越了 Context 限制,第一步操作后新状态仍然是 50 个文件的完整内容——下一轮又需要重新加载。搜索复杂度 是数学上的必然结论:这不是”模型不够聪明”,而是在这个问题空间定义下,任何搜索策略都面临指数级代价。
结果:Agent 在第一轮耗尽 Context 预算,任务未完成。失败不来自 Agent 的推理能力,而来自问题空间建模使搜索从一开始就处于不可行的量级。
建模 B:精简状态 × 有界操作集
重新设计 的四个组件:
(精简初始状态,约 800 tokens):
{
"task": "rename UserManager → AccountService",
"references": null, # 待填充:grep 后更新
"interface_def": "<UserManager 的类签名与公开方法,约 30 行>",
"test_results": "12 passed, 0 failed",
"progress": {"renamed": [], "updated": [], "remaining": "TBD"}
}
(6 个有界操作):
grep_references(name)→ 返回包含该名称的文件列表,更新references字段rename_class(file, old, new)→ 重命名类定义本身update_imports(file, old, new)→ 更新 import 语句update_calls(file, old, new)→ 更新实例化与调用点run_tests()→ 返回当前测试通过/失败列表,更新test_resultscommit(message)→ 提交当前变更
:test_results.failed == [](可自动验证)。
搜索结构:分支因子 :在任意状态下,通常只有 1-3 个操作是语义上合理的下一步(grep 完成后,下一步显然是处理列表中的第一个文件)。有效分支因子 。路径深度 :1(grep)+ 12(逐文件更新)+ 1(运行测试)+ 1(提交)= 15 步。搜索复杂度 ——理论上仍大,但 Plan 提供了近乎确定性的步骤序列,实际有效搜索节点数 < 30。
执行过程:
第一步,Agent 调用 grep_references("UserManager"),状态更新为 12 个文件的路径列表(约 80 tokens),progress 中 remaining 被填充。此后每步操作将一个文件从 remaining 移至 updated,状态大小始终保持在 ~800 tokens。第 14 步调用 run_tests(),得到”12 passed, 0 failed”, 满足,Stop Hook 触发终止。
两种建模的定量对比
| 维度 | 建模 A | 建模 B |
|---|---|---|
| 初始状态大小 | ~45,000 tokens | ~800 tokens |
| 操作集规模 | ~1,000(实际无界) | 6 |
| 有效分支因子 | ~1,000 | ~3 |
| 解路径深度 | ~10(理论下界,未实现) | ~15(实际) |
| 搜索复杂度 | ,不可行 | ,Plan 引导后 < 30 节点 |
| Q(任务完成率) | 0%(Context 第一轮耗尽) | ~95%(测试通过验证) |
| T(完成时间) | 失败,无法计量 | ~4 分钟 |
| C(总 token 消耗) | 第一轮即超预算 | ~12,000 tokens |
| 目标 的可验证性 | 未定义 | 自动验证(测试通过) |
两种建模的核心差异:工程师的设计决策
建模 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 运行之前做出。问题空间的结构是工程的产物,而非任务的自然属性——同一个重构任务,可以被建模为不可行问题,也可以被建模为十五步确定性流程。搜索失败之前,往往先发生了建模失败。