Agent 不一定要自由规划:我给 Tasklist Agent 加了一次受控决策

本文对应项目版本:v0.1.1

把固定流程 Agent,改造成一次有限决策的受控 Agent

我在做 AI Mind v0.1.1 时,刻意没有把 Agent 变得更自由。

现在很多 Agent 文章会默认一个方向:让模型自己规划、自己选工具、自己决定下一步,最好还能循环执行。但这个版本里,我做的是另一件事:只给任务清单 Agent(Tasklist Agent)加一次受控决策。

AI Mind 是我按版本持续演进的 AI Native Runtime Skeleton。它不是普通聊天 Demo,也不是完整 Agent 平台,而是围绕聊天运行时、流式协议、工具调用、Skill、MCP 和 Agent 一层一层搭出来的工程实验。

到 v0.1.0 时,项目已经有了第一个受控 Tasklist Agent:用户通过 /tasklist + @docs://versions/*.md 显式引用版本方案后,运行时会读取方案、生成任务清单草稿、执行结构校验,并在必要时最多自动修正一次。

但 v0.1.0 的 Agent 本质上还是固定流程。

固定流程的好处是稳:入口明确、资源明确、工具明确、最多修一次,普通问答链路不会被污染。问题也很明显:如果版本方案有弱项、缺少一点上下文,或者用户输入已经越过 Tasklist Agent 的边界,固定流程就不够灵活。它要么继续生成,要么失败,很难在中间做一次"可控判断"。

所以 v0.1.1 的主题可以概括为:一次受控规划决策。它只在原有受控链路里增加一次规划决策(Planning Decision):让模型在运行时白名单允许的 5 类动作中选择下一步。

如果只看真实执行链路,v0.1.1 的 Tasklist Agent 可以先简化成这张图:

flowchart TD A["/tasklist + 版本方案引用"] --> B{"入口命中?"} B -- "否" --> B1["fail closed<br/>提示先引用 docs://versions/*.md"] B -- "是" --> C["读取版本方案<br/>并判断方案就绪度"] C --> D["执行规划决策<br/>让模型在 5 类安全动作(action)里选一个"] D --> E{"受控动作(action)"} E -- "缺少关键版本目标<br/>选择澄清动作" --> S["进入停止状态<br/>本轮不生成草稿"] E -- "输入超出 Agent 能力边界<br/>选择边界停止动作" --> S E -- "需要补一点项目背景<br/>选择读取可选上下文动作" --> F["补读 1 个白名单上下文<br/>失败则带复核点继续"] E -- "方案可继续<br/>选择继续或带复核继续动作" --> G["生成任务清单拆分策略<br/>控制粒度、分组和优先级"] F --> G G --> H["生成任务清单草稿 v1"] H --> I["结构校验<br/>validate_tasklist_structure"] I --> J{"警告分流<br/>WarningDisposition"} J -- "需要立即修正<br/>fixNow > 0" --> K["自动修正一次生成 v2<br/>并再次校验"] J -- "通过 / 仅人工复核" --> L["评估修正效果<br/>RevisionEffectResult"] K --> L L --> M["最终输出<br/>Trace 展示过程<br/>Artifact 展示 tasklist"]

先看结论

这版的核心判断可以压成一句话:

text 复制代码
只放开 action 选择,
不放开资源权限、工具权限、写入权限和循环权限。

模型可以选择下一步,但只能从运行时给出的安全动作里选。它不能自己决定读取源码,不能扫描目录,不能写入文件,也不能无限修正。

这比纯固定流程更灵活,也没有把整个系统推向开放式 Planner。

v0.1.0:固定流程为什么还不够

v0.1.0 的主链路很直接:

text 复制代码
读取版本方案
-> 生成任务清单草稿 v1
-> 执行结构校验
-> 必要时自动修正 v2
-> 再次校验
-> 输出最终回答

这条链路已经能完成一个受控 Agent 的基本闭环。但真实版本方案并不总是完美的。

有些方案信息完整,可以直接生成。

有些方案主体可用,但缺少测试计划、非目标或风险说明,应该继续生成,但最终提醒人工复核。

有些方案缺少核心目标,继续生成会变成模型脑补。

还有些输入根本不该进入 Tasklist Agent,比如从裸目标生成任务清单、自动读取源码、写入 docs、扫描历史任务清单。

固定流程很难细分这些情况。v0.1.1 想解决的不是"让 Agent 彻底自由",而是让它在关键位置多一次判断。

第一层控制:先判断方案够不够用

在让模型做选择之前,v0.1.1 先加了一层方案就绪度判断(Plan Readiness)。

它回答一个问题:

text 复制代码
这份版本方案,够不够用来生成任务清单?

这一步不调用模型,而是由运行时用规则判断。结果只有三类:

ts 复制代码
export interface PlanReadinessResult {
    // 缺失字段,比如 targetVersion、Goals、tasklistBasis。
    missingFields: string[]
    // 给 Trace 和最终摘要使用的中文原因。
    reason: string
    // ready:可继续;needs_review:可继续但需要复核;blocked:不可继续。
    status: 'blocked' | 'needs_review' | 'ready'
    // 弱项字段,比如 Non-goals、Interface Changes、Test Plan。
    weakFields: string[]
}

比如目标版本无法识别、正文过短、缺少可拆分目标,就会进入不可继续(blocked)。如果只是缺少测试计划或非目标,则可以继续,但需要在最终结果里提示人工复核。

这一步很重要。模型不是从一片空白开始规划,而是拿到运行时已经整理好的输入判断。

换句话说,方案就绪度不是为了让 Agent 更聪明,而是先给 Agent 画出第一道边界。

第二层控制:模型只能选择 5 种动作

规划决策(Planning Decision)是 v0.1.1 的核心变化。

但这里的"规划"不是自由规划。模型只能从 5 种动作中选一个,真实代码里的合同是这样的:

ts 复制代码
export type PlanningDecisionAction =
    | {
          // 方案足够完整,继续进入任务清单拆分策略。
          reason: string
          type: 'proceed_to_tasklist_strategy'
      }
    | {
          // 只允许读取一个运行时白名单里的补充上下文。
          reason: string
          resourceUri: VersionPlanTasklistOptionalContextResourceUri
          type: 'read_optional_context'
      }
    | {
          // 缺一个关键答案时,本轮输出澄清问题并停止。
          question: string
          reason: string
          type: 'ask_clarification'
      }
    | {
          // 可以继续生成,但把弱项沉淀成最终人工复核点。
          reason: string
          reviewItems: VersionPlanTasklistManualReviewItem[]
          type: 'proceed_with_manual_review_items'
      }
    | {
          // 输入越过 Tasklist Agent 边界,本轮输出边界提示并停止。
          message: string
          reason: string
          type: 'stop_with_boundary_message'
      }

这 5 个动作覆盖了固定流程最需要分岔的场景。

方案完整,就继续生成。

方案基本可用但需要补一点背景,就读取一个可选上下文。

缺少关键答案,就提出澄清问题。

存在弱项但不阻塞,就带人工复核点继续。

输入越界,就停止。

重点是:模型只能选择方向,不能扩大边界。

第三层控制:状态机决定动作能不能执行

只限制模型输出还不够。

如果模型选择"读取可选上下文(read_optional_context)",运行时还要继续判断:当前状态真的允许读取吗?读取的是不是白名单资源?之前有没有已经读过一次?

这就是状态机的作用。

状态机可以简单理解成一套"当前处在哪个阶段、下一步允许做什么"的规则。

它常见于流程边界很明确的系统里,比如订单状态流转、审批流程、任务执行器、文件上传流程,或者像这里这样的 Agent 执行链路。

它解决的不是"怎么生成内容",而是"系统现在走到哪一步,接下来哪些动作是合法的"。

在 v0.1.1 里,状态机就是 Agent 的执行路线图。它不负责生成文本,也不负责理解语义,它只回答一个很硬的问题:

text 复制代码
当前状态下,这个动作能不能执行?

v0.1.1 的状态不是随便记几个字符串,而是把 Agent 的关键阶段显式拆开:

ts 复制代码
export type VersionPlanTasklistAgentStatus =
    | 'idle'
    | 'plan_read'
    | 'readiness_checked'
    | 'planning_decided'
    | 'optional_context_read'
    | 'strategy_decided'
    | 'drafted_v1'
    | 'validated_v1'
    | 'revised_v2'
    | 'validated_v2'
    | 'revision_effect_evaluated'
    | 'stopped'
    | 'final'

这些状态不是为了把实现写得复杂,而是为了把边界说清楚:

  • idle:刚开始,只能读取用户显式引用的版本方案。
  • plan_read:已经读到版本方案,可以检查方案就绪度。
  • readiness_checked:已经完成规则判断,可以做一次规划决策。
  • planning_decided:模型已经选择了下一步,运行时才允许继续推进。
  • optional_context_read:已经读取最多一个可选上下文。
  • strategy_decided:已经确定任务清单拆分策略,可以生成草稿。
  • stopped:已经澄清或边界停止,不能继续生成。

这带来一个很重要的效果:模型说了不等于系统就做。

比如读取资源这个动作,真实代码里会先看当前状态:

ts 复制代码
if (state.status === 'idle') {
    // 第一轮只能读取用户显式引用的 version plan。
    return action.resourceUri === state.versionPlanReference.uri
        ? { success: true }
        : { success: false }
}

if (state.status !== 'planning_decided') {
    // 补充上下文只能发生在规划决策之后。
    return { success: false }
}

const planningDecision = state.artifacts.planning.decision

if (planningDecision?.type !== 'read_optional_context') {
    // 只有 read_optional_context 决策授权后才能补读上下文。
    return { success: false }
}

这段逻辑解决的是一个常见 Agent 风险:模型一旦觉得"多读一点可能更好",系统就不断打开新的资源入口。

在 v0.1.1 里不会这样。只有当规划决策明确选择读取可选上下文,并且当前状态已经进入 planning_decided,运行时才会允许读取。

而且状态机还会继续检查资源 URI、读取次数和最大步数。

ts 复制代码
if (action.resourceUri !== planningDecision.resourceUri) {
    // 只能读取规划决策指定的那个资源,不能临时换一个。
    return { success: false }
}

if (state.counters.optionalContextReads >= state.limits.maxOptionalContextReads) {
    // 可选上下文最多读取一次。
    return { success: false }
}

if (state.counters.steps >= state.limits.maxSteps) {
    // Agent 总步数也有上限。
    return { success: false }
}

这里的状态机不是"流程图装饰",而是运行时硬边界。

它控制了四类事情:

text 复制代码
顺序:必须先读方案,再判断就绪度,再做规划决策。
权限:只有特定决策能触发特定资源读取。
次数:可选上下文最多一次,自动修正最多一次。
停止:澄清、边界停止或达到上限后,不允许继续往后跑。

受控 Agent 的控制力,不来自一句 prompt,而来自运行时对每一步的硬约束。

模型可以提出选择,但状态机决定这个选择能不能执行。

读取可选上下文:只补读一个白名单资源

规划决策里有一个动作叫读取可选上下文(read_optional_context)。

它的设计目标很克制:如果版本方案基本可用,但补一份项目背景或架构边界能让拆分更稳,运行时允许 Agent 读取一个白名单资源。

白名单不是模型自己决定的,而是代码里固定的:

ts 复制代码
export const VERSION_PLAN_TASKLIST_OPTIONAL_CONTEXT_RESOURCE_URIS = [
    'docs://README.md',
    'docs://architecture/agent-runtime.md',
    'docs://architecture/capability-skill-surface.md',
    'docs://architecture/runtime-boundary.md',
    'docs://architecture/stream-core.md',
    'project://latest-context',
] as const

这意味着模型即使选择了读取可选上下文,也只能在这几个资源里选一个。

它不能读源码目录,不能读历史任务清单,不能扫描版本方案目录,也不能把这一步扩展成开放式资源选择。

读取失败也不会让整个 Agent 崩掉。运行时会降级继续:仍然基于原始版本方案生成任务清单,同时把"补充上下文读取失败"加入人工复核点。

这一步的目标不是让 Agent 无限制知道更多,而是只在需要时补读一个受控上下文。

拆分策略:结构化地影响下一次生成

这里要说清楚一点:任务清单拆分策略最终仍然是通过提示词(prompt)影响下一次模型生成。

它不是一个单独的执行器,也不会直接修改草稿内容。

但它和普通"多写几句提示词"不一样。v0.1.1 先让模型输出结构化的任务清单拆分策略(TasklistStrategy),运行时会校验它、保存它,然后在生成任务清单草稿时,把它明确注入 draft prompt。

所以这里的关键不是"脱离 prompt",而是让 prompt 增强变得受控:

  • 策略字段是固定的。
  • 输出必须通过 schema。
  • 策略会写入本轮 AgentState。
  • 后续 draft prompt 必须消费它。
  • 它只能影响粒度、步骤范围、分组和优先级,不能扩大版本方案范围。

真实代码里的策略结构是这样的:

ts 复制代码
export interface TasklistStrategy {
    // 预计生成多少个 Step。
    expectedStepRange: [number, number]
    // 拆分粒度:粗粒度、细粒度或中等粒度。
    granularity: 'coarse' | 'detailed' | 'medium'
    // Step 标题和章节优先围绕这些分组组织。
    grouping: string[]
    // checklist 和执行顺序优先遵循这些优先级。
    priority: string[]
    // 说明为什么这样拆。
    reason: string
}

在生成任务清单草稿时,它会被写进草稿生成提示词,变成明确的生成约束:

text 复制代码
任务清单拆分策略:
- Step 数量尽量落在 expectedStepRange 之间。
- Step 标题和顺序优先围绕 grouping 组织。
- 每个 Step 内 checklist 的先后顺序优先遵循 priority。
- 拆分粒度遵循 granularity。
- 不得新增版本方案中没有的能力范围。

所以,TasklistStrategy 不是在替代 prompt,而是在把 prompt 增强从"自由文本建议"变成"结构化、可校验、由运行时注入的生成条件"。

规则和模型怎么分工

v0.1.1 的分工可以概括成:

text 复制代码
模型负责选择和生成,
运行时负责边界和判断。

模型负责语义弹性更强的部分:选择下一步、生成澄清问题、生成边界提示、生成拆分策略、生成草稿、修正一次草稿。

规则负责确定性更强的部分:方案是否够用、动作能不能执行、上下文是否在白名单、警告该自动修还是人工复核、修正是否有效、最多走多少步。

如果所有判断都交给模型,系统会更"聪明",但也更难测试、更难复现。

如果所有东西都写成固定规则,系统会很稳,但不够灵活。

v0.1.1 选择的是中间路线:让模型参与一次方向判断,但把边界控制留给运行时。

不是所有 warning 都应该自动修

任务清单草稿生成后,会执行结构校验。

v0.1.0 的规则比较粗:发现问题后最多自动修一次。v0.1.1 则增加了警告分流(WarningDisposition),把问题分成两类:

ts 复制代码
export interface WarningDisposition {
    // 需要立即自动修正的问题编号。
    fixNow: string[]
    // 不自动修,但要展示给用户确认的问题。
    manualReviewItems: VersionPlanTasklistManualReviewItem[]
    // 本次分流的说明。
    reason: string
}

结构硬伤,比如缺少实施步骤、缺少清单项、缺少工程验证,进入立即修正(fixNow)。

质量弱项,比如风险说明偏弱、暂停点需要人工确认,则进入人工复核点(manualReviewItems)。

这一步很小,但很有工程意义。不是所有"不完美"都应该交给模型继续修。有些不确定性,明确告诉人看一眼,反而更可靠。

修了以后,还要知道有没有变好

如果触发自动修正,v0.1.1 还会做修正效果评估(Revision Effect)。

它会比较 v1 和 v2 的结构校验结果:

ts 复制代码
export interface RevisionEffectResult {
    // 最终结论:可采用、需人工复核,或阻塞。
    finalDecision: 'blocked' | 'final' | 'final_with_manual_review_items'
    // 已经修掉的问题编号。
    fixedIssues: string[]
    // 分数是否提升。
    improved: boolean
    // 仍然存在的问题编号。
    remainingIssues: string[]
    // 修正后分数。
    scoreAfter: number
    // 修正前分数。
    scoreBefore: number
}

这里同样不让模型自由总结,而是基于稳定的问题编号比较。

如果 v2 仍然失败,最终结论会变成阻塞(blocked),并提示当前草稿不建议直接采用。但这一版不会继续生成 v3。

原则很明确:

text 复制代码
可以自动补一轮结构问题,
但不能把自动修正变成无边界循环。

Trace 展示过程,Artifact 展示产物

后端有状态机、有规则、有质量门,如果前端只展示最后一段文本,用户其实感知不到这些过程。

所以 v0.1.1 继续强化了 Agent 执行过程面板(AgentTracePanel)。它展示的是轻量摘要,而不是完整调试日志:

text 复制代码
方案是否就绪
规划决策选择了什么动作
是否读取了可选上下文
拆分策略的粒度和步骤范围
警告分流结果
修正效果
最终决策

同时,最终任务清单正文不再混在普通聊天回答里,而是通过 Agent 文本产物面板(AgentTextArtifactPanel)独立展示。

最终回答分成三层:

text 复制代码
执行过程:AgentTracePanel
最终产物:AgentTextArtifactPanel
摘要说明:普通 assistant text

这个分工对 AI 应用前端很重要。Agent 类应用不能只设计"最后答案"。过程应该可追踪,产物应该可复制、可独立阅读,摘要应该帮助用户判断当前产物是否能用。

这一版刻意收住了什么

更准确地说,v0.1.1 只验证一件事:

text 复制代码
在固定流程里开放一次受控选择权,
但不顺手放开资源、工具、写入、循环和持久化。

所以这一版把下面这些能力先放在边界外:

  • 不把 Tasklist Agent 扩成通用 Agent Runtime 或多 Agent。
  • 不用 LangGraph 替换当前 Runtime,也不做开放式规划执行。
  • 不开放自由工具调用,仍然只允许结构校验这个受控工具。
  • 不自动扫描版本目录,不读取历史任务清单,也不从裸目标直接生成任务清单。
  • 不写入 docs 文件,最终只输出可复制的任务清单草稿。
  • 不做暂停 / 恢复 / 检查点,也不持久化 AgentState。
  • 不生成 v3 自动修正,不把 Artifact 做成产物工作台。

这样写边界不是为了证明"还没做什么",而是为了保护本版主题:当模型只有一次选择权时,运行时能不能仍然把这次选择控制住。

这版证明了什么

v0.1.1 最重要的结论,不是"模型会规划了"。

更准确地说,它证明的是:

text 复制代码
一个固定流程 Agent,可以先演进成有限决策 Agent。

这中间不一定要一步跳到自由规划。

在这版里,模型只做一次选择。选择范围只有 5 种。资源只能读白名单。工具只有结构校验。自动修正最多一次。最终不写入文件。什么时候停止,由状态机决定。

但即便限制这么多,Agent 的行为已经比固定流程更灵活。

它可以在方案太弱时提出澄清问题。

它可以在输入越界时停止。

它可以在方案可用但有风险时带人工复核点继续。

它可以在需要时补读一个受控上下文。

它可以用拆分策略影响最终草稿。

这就是"一次受控规划决策"的价值:在受控系统里增加一点可解释的弹性。

后续方向

v0.1.1 之后,我更想继续沿着"受控增强"的路线推进。

后续更值得评估的是:

text 复制代码
Agent Trace 持久化
任务清单草稿保存与人工确认流
更完整的 Agent 动作执行器
和 LangGraph 做边界清晰的对照实验

这些都不应该回填到 v0.1.1。

这一版已经完成了它该完成的事情:让第一个 Tasklist Agent 从固定流程,迈到一次有限决策。

结尾

AI Mind v0.1.1 做的是一件很小、但对我很关键的事:在一条已经受控的任务清单生成链路里,让模型获得一次有限选择权,再用运行时状态机、白名单、规则判断、质量门和前端展示把这次选择落住。

这版真正让我确认的是:

text 复制代码
Agent 工程化不一定从"自由规划"开始,
也可以从"固定流程里的第一次受控分岔"开始。

这也是我目前更认可的 Agent 演进路线:

text 复制代码
先让它可控,
再让它有限选择,
最后再讨论更开放的规划执行。

项目地址:

github.com/HWYD/ai-min...

如果你也在关注 AI 应用前端、运行时编排、工具调用、MCP 或 Agent 工程,可以顺手点个 Star。这个项目会继续按版本迭代,每一版都会尽量把源码、设计文档、任务清单、发布记录和复盘文章一起留下来。

相关推荐
阿里云云原生8 小时前
可观测性的终局?从“面向数据”到“面向对象”,UModel 如何为 AI Agent 注入认知地图
云原生·agent
LienJack8 小时前
《Re0 Build Harness》第一章 Agent 基础定义:它为什么不是一句 Prompt?
aigc·agent
青云计划8 小时前
Host 组件:从混乱到秩序的多Agent系统总指挥
agent
Puslar9 小时前
Agent系列二:项目架构设计
agent·全栈
PPPHUANG9 小时前
我把 MacBook 的 Touch Bar,改造成了 AI "摸鱼状态灯"
openai·agent·ai编程
SelectDB9 小时前
- 别把懂语义和查事实混为一谈:企业级 Agent 真正缺的是什么?
数据库·数据分析·agent
C咖咖10 小时前
code review agent
agent·code·review
Wu_Dylan12 小时前
造一个 AI Skill 的 Lighthouse:SkillScope 架构设计与工程实践
agent
眼眸流转12 小时前
Dify学习笔记
笔记·学习·agent·dify