跳到主要内容

Skyvern 2.0 规划器:从一句话目标到自主编排

30 秒导读: README 说 Skyvern 用「a swarm of agents(一群 agent)」去理解网页、规划并执行——这句话的真身就是本章讲的 task_v2。它不是真有很多进程在跑,而是一个外循环:拿到你一句话的大目标,每一轮先看当前页面,让 LLM 规划「下一步该干的小目标」,把这个小目标当场物化成一个工作流 block(导航 / 提取 / 循环)去执行,再把结果塞回历史、进入下一轮,直到 LLM 判定目标达成或不可能。

本章建立在前面几章之上,只讲新增的那层自治外循环,不重复它们的内部:


1. 这是什么(零基础也能懂)

一句话定义: task_v2(代码里也叫 observer cruise / Skyvern 2.0)是一个会自己写工作流的 agent——你只给一句话目标,它自己一步步把工作流「生长」出来并执行完。

它和 task_v1 的区别,是本章的核心。 用一句话说清:

  • task_v1(单任务): 你给一个明确任务("在这个页面上填表并提交"),它在固定的 step 循环里把这一件事做完。任务边界是你划的。
  • task_v2(自主规划): 你给一个大目标("帮我在这三家店里比一款笔记本的价格,选最便宜的"),它自己决定要分成哪些小任务、每个小任务是什么类型、按什么顺序做。任务边界是它自己划的。

给谁用 / 解决什么问题: 给那种「一句话说得清、但拆成步骤很烦」的网页活。你不想(也没法)预先画好工作流图,就把大目标丢给它,让它边看边想边做。

用起来什么样: 对外就是一句话 + 可选的起始 URL。

# 示意,非源码:task_v2 对外的最小心智模型
run = skyvern.run_task(
prompt="在 example-shop 上找到评分最高的三款机械键盘,比较价格,返回最便宜的那款",
engine="skyvern-2.0", # 走 task_v2 规划器,而不是 v1 单任务
)
# 剩下的拆解、导航、提取、比较,全由规划器自己完成

一句话直觉: 把它想成一个边走边画流程图的人。他手上只有一句话目标,每走到一个新页面就停下来想「下一格该画什么框」,画完一个框(block)就立刻执行,看结果,再想下一格——而不是像 v1 那样照着一张已经画好的图走。

本节不碰底层。记住一件事就够了:v2 = 在工作流引擎之上,加一层「LLM 动态生成下一个 block」的自治外循环。


2. 顶层全景(它大概怎么转)

2.1 一张图看懂外循环

规划器的心脏是 run_task_v2_helper 里的一个 for i in range(max_iterations) 循环(skyvern/services/task_v2_service.py:744)。每一轮(iteration)都做同样四件事:

一句话大目标 (user_prompt)


┌───────────────────────────────────────────┐
│ 每一轮 iteration(最多 50 轮) │
│ │
│ ① 观察 抓取当前页面 DOM + 截图 │
│ (scrape_website) │
│ │ │
│ ▼ │
│ ② 规划 LLM 读 页面+历史+大目标 │
│ → 判定 达成? 放弃? 还是 │
│ 下一个 mini goal + task_type │
│ (prompt: task_v2.j2) │
│ │ │
│ 达成 ──┴── 放弃 │
│ │ │ │
│ ▼ ▼ 否则继续 ▼ │
│ summarize terminate │
│ (完成) (终止) ③ 物化 │
│ 把 mini goal 变成 │
│ 一个 workflow block │
│ (_generate_*_task) │
│ │ │
│ ▼ │
│ ④ 执行 block │
│ (block.execute_safe) │
│ │ │
│ ▼ │
│ 结果写入 task_history │
│ 成功则再跑一次完成校验 │
│ (task_v2_check_completion) │
└──────────────────────────┬──────────────────┘
│ 回到 ① 进入下一轮

达成 / 放弃 / 超步数 / 超轮数 → 终态

怎么读这张图: 从上往下是一轮的四步「观察 → 规划 → 物化 → 执行」,右边的分叉是三种提前退出(达成、放弃、超预算)。只要没退出,就回到 ① 重新观察,进入下一轮。

2.2 部件一句话职责

部件干什么在哪里
initialize_task_v2建空工作流 + workflow run 的壳,把 task_v2 挂上去task_v2_service.py:261
initialize_task_v2_metadata开跑前先让 LLM 推断起始 URL 和工作流标题task_v2_service.py:384
run_task_v2外层入口:铺好 SkyvernContext、兜底异常、收尾清理浏览器task_v2_service.py:481
run_task_v2_helper核心主循环:观察→规划→生成→执行,逐轮推进task_v2_service.py:604
_generate_navigation_task把「导航类」mini goal 物化成 NavigationBlocktask_v2_service.py:1725
_generate_extraction_task把「提取类」mini goal 物化成 ExtractionBlocktask_v2_service.py:1645
_generate_loop_task把「循环类」mini goal 物化成 ForLoopBlocktask_v2_service.py:1411
_generate_goto_url_task生成一个纯跳转的 UrlBlock(第 0 轮开局用)task_v2_service.py:1763
handle_block_result看 block 执行结果,决定工作流 run 是否要取消 / 刷新状态task_v2_service.py:1316
mark_task_v2_as_*五个终态落库:completed / failed / terminated / canceled / timed_outtask_v2_service.py:1849+
task_v2.j2规划 prompt:读页面判定下一步、是否达成、是否放弃forge/prompts/skyvern/task_v2.j2
task_v2_check_completion.j2完成校验 prompt:每次 block 成功后复核大目标是否真达成forge/prompts/skyvern/task_v2_check_completion.j2

2.3 主线走一遍(高层,不进代码)

  1. 你给一句话(+ 可选 URL)。initialize_task_v2 建一个空工作流——注意,此刻工作流里一个 block 都没有
  2. initialize_task_v2_metadata 先让 LLM 猜「该从哪个 URL 开始、这活儿叫什么名字」。
  3. 进入主循环。第 0 轮通常先生成一个跳转 block 把浏览器带到起始页。
  4. 之后每一轮:抓当前页 → 规划 prompt 吐出「下一个 mini goal + 它是 navigate/extract/loop 中的哪一种」→ 把它物化成对应 block 追加进工作流 → 执行 → 结果进历史。
  5. 规划 prompt 哪一轮说「user_goal_achieved=true」,就总结产出、收尾完成;说「should_terminate=true」就判定不可能、终止。
  6. 撞到轮数上限或步数上限,则失败收场。

关键认知:工作流是被这个循环一轮一轮「长」出来的,不是预先定义好的。 这就是 v2 相对 v1 的本质增量。


3. 核心原理(逐个机制,由浅入深)

3.1 开局:先建空壳,再让 LLM 补上 URL 和标题

要解决的小问题: 你可能只给了一句话,连从哪个网址开始都没说。得有人把「起点」补出来。

思路: 分两步。initialize_task_v2 先把承载执行的基础设施建好——一个空工作流 + 一次 workflow run(task_v2_service.py:313create_empty_workflow:323setup_workflow_run)。真正的「起始 URL / 标题」留给 LLM 在 initialize_task_v2_metadata 里推断:

  • task_v2_generate_metadata prompt,喂大目标和当前浏览器 URL;
  • 拿回 urltitle,装进 TaskV2Metadatatask_v2_service.py:415-422);
  • 若既没用户给的 URL、LLM 也没推断出,直接抛 UrlGenerationFailure:416-417)——没有起点就没法跑

关键细节: 推断结果会回写工作流标题(原来是占位的 "New Workflow"),所以你在 UI 上看到的工作流名,是 LLM 现起的。

3.2 主循环:一轮 = 观察 + 规划 + 物化 + 执行

这是整章的骨架,全在 run_task_v2_helpertask_v2_service.py:604)。循环体 for i in range(max_iterations):744 开始。拆开看:

① 观察。 每轮开头重新拿浏览器页面并抓取(scrape_website:840),把 DOM 元素树 + 截图备好。感知细节见 01-perception-scraper.md

② 规划。load_prompt_with_elements 组装 task_v2 规划 prompt(:859),喂进去的关键料是三样:

  • current_url:当前在哪
  • user_goal:一句话大目标
  • task_history到目前为止做过的所有小任务及其结果

然后调 LLM(:881),解析出这几个决策字段(:896-903):

user_goal_achieved → 大目标是否已经达成
should_terminate → 是否判定不可能、该放弃
plan → 下一个 mini goal 的文字描述
task_type → 这个 mini goal 属于 navigate / extract / loop 哪种

③ 分派与物化。task_type 走不同分支,把 plan 这段文字变成一个真的 block

task_type物化成生成函数
navigateNavigationBlock_generate_navigation_task:987
extractExtractionBlock_generate_extraction_task:966
loopForLoopBlock_generate_loop_task:1000

navigate 分支还会用 MINI_GOAL_TEMPLATE 把 mini goal 包进大目标做上下文(:989,模板见 constants.py:92),让底层单步 agent 知道「这一小步是为了哪个大目标」。

④ 追加进工作流并执行。 生成的 block 的 YAML 被 extend 进累积列表,然后整份工作流被重新创建:1035-1057create_workflow_from_request)——这就是工作流「一轮长一格」的物理动作。接着 block.execute_safe:1061)真正执行。Block 的执行内部见 05-workflow-blocks.md

⑤ 回灌历史。 执行结果(状态、失败原因、提取到的数据)打包成 task_history_record 追加进 task_history:1101)。下一轮规划时,这段历史又会被喂回 prompt——这就是外循环「记得自己做过什么」的机制。

# 示意,非源码:一轮的骨架,对应 run_task_v2_helper
for i in range(max_iterations):
scraped = scrape(current_page) # ① 观察
plan = llm(task_v2_prompt(scraped, goal, history))# ② 规划
if plan.user_goal_achieved: summarize(); break # 达成→完成
if plan.should_terminate: terminate(); return # 放弃→终止
block = materialize(plan.task_type, plan.text) # ③ 物化成 block
result = block.execute() # ④ 执行
history.append(record(plan, result)) # ⑤ 回灌历史
# 重点看:history 每轮都被喂回 ② 的 prompt,这是自治的关键

3.3 规划 prompt:三种 task_type 与「循环优先」

要解决的小问题: LLM 怎么知道下一步该导航、该提取、还是该开一个循环?

思路: 全写在 task_v2.j2(规划 prompt)里,它给 LLM 定义了三种动作及其分工:

task_type干什么边界(prompt 明确划的)
navigate到达正确页面 / 在页面上做操作(填表、点按钮、选项)不负责抓数据,别把 navigate 写成「…并提取数据」
extract从当前页面抓信息只抓不导航,页面保持静止
loop对一批同形目标做同一件事(N 个链接、N 行、N 个商品)广度优先,一个 loop 顶 N 个串行任务

一个关键设计——「循环优先」(task_v2.j2:6-13)。 prompt 反复强调:只要目标是「对多个同形项做同一操作」,就用一个 loop,别拆成 N 个串行的 navigate/extract。原因很实在:串行 N 个会耗尽轮数和步数预算,常常没做完就超预算了;一个 loop 在一步规划里就覆盖全部 N 项。

另一个精妙的反死循环设计(task_v2.j2:46-49)。 prompt 明确告诉 LLM:最终的总结/比较/推荐是达成时自动生成的,没有任何 task 负责写它。所以别去规划「把结果总结一下」这种 extract 任务——extract 只会一遍遍返回页面数据而非综合结论,这是「跑光步数预算」最常见的原因。只要各部分需要的数据都被 extract 抓到了,就该置 user_goal_achieved=true 收尾。

3.4 loop 的物化:最复杂的一支,两次 LLM 调用

要解决的小问题: 生成一个循环,得先知道「循环谁」(哪一批值),再知道「对每个值做什么」。

思路: _generate_loop_tasktask_v2_service.py:1411)分两步,各调一次 LLM:

  1. 先跑一个提取 block 把「循环值」抓出来。task_v2_loop_task_extraction_goal prompt 生成提取目标,schema 由 _generate_data_extraction_schema_for_loop:99)固定成 {loop_values: [...], is_loop_value_link: bool}。这个 ExtractionBlock 当场执行:1465),拿到要迭代的那批值。
  2. 再规划「循环体里那一个任务」。task_v2_generate_task_block prompt(:1549)生成循环内任务的 navigation_goal / data_extraction_goal / data_schema,物化成一个 TaskBlock,再包进 ForLoopBlock:1617-1633)。

关键细节 / 坑:

  • is_loop_value_link 决定循环体怎么用值:是链接就每轮先跳过去(key 前缀 task_in_loop_url_),不是链接就当参数注入(key target_)。
  • 非链接的参数 key 特意加了随机后缀(:1528-1530),否则一个 run 里多个非链接循环都叫 target,会撞「重复 key」校验。这类 corner case 是踩过坑后补的。

3.5 完成校验:block 成功后再复核一次大目标

要解决的小问题: 规划 prompt 在执行前说「还没达成」,但一个 block 成功执行之后,局面可能已经变了。不能只信执行前的判断。

思路: 每当 block success 为真,主循环会再跑一次独立的完成校验task_v2_service.py:1123-1230):重新抓页面截图,喂 task_v2_check_completion prompt,专门问「就凭现在的 task_history + 截图,大目标达成没有 / 是不是不可能」。

这是一个「执行前规划、执行后复核」的双保险:

  • 规划 prompt(task_v2.j2)在轮首判断要不要继续
  • 完成校验 prompt(task_v2_check_completion.j2)在 block 成功后判断是不是可以收工了

两个 prompt 都带同一套严格约束:多部分目标必须每一部分都满足才算达成(required_subgoals 逐条核对);终止要极其保守,必须页面上有明确「不可能」的证据才 should_terminate

3.6 思考记录:thought / plan 的可观测轨迹

要解决的小问题: 这么多轮 LLM 决策,用户和调试者得能看见它「想了什么」。

思路: 每次 LLM 决策前后,都往数据库写一条 Thought。类型和场景是枚举(schemas/task_v2.py:131ThoughtType:147ThoughtScenario):

时机ThoughtTypeThoughtScenario
规划下一步plangenerate_plan
推断 URL/标题metadatagenerate_metadata
完成校验user_goal_checkuser_goal_check
抽取循环值planextract_loop_values
生成循环内任务internal_plangenerate_task_in_loop
终止terminationtermination
总结产出user_goal_checksummarization

规划那条 thought 在 LLM 返回后被 update_thought 填上 thoughts(推理)、observation(页面观察)、answer(plan 文本)和结构化 outputtask_v2_service.py:905-917)。UI 时间线就是读这些 thought 渲染出来的(get_thought_timelines:1791)。


4. 深入实现(要读源码的人看这里)

4.1 两条预算线:轮数 vs 步数

v2 有两个独立的预算,任一耗尽都失败,别混淆:

预算含义默认 / 来源检查点
max_iterations规划轮数:外循环最多转几圈DEFAULT_MAX_ITERATIONS = 50task_v2_service.py:89for i in range(max_iterations):744);跑满走 for...else:1278
max_steps总步数:所有生成的 block 里累计的底层 step 数settings.MAX_STEPS_PER_TASK_V2:738每轮末尾累加校验(:1232-1238

_resolve_max_iterations:587)有个防退化设计:外部传入的 override 会和历史下限 DEFAULT_MAX_ITERATIONSmax,保证老工作流不会因为存了更小的旧默认值而倒退。

踩坑提示: 「规划得很聪明但每步很慢」会先撞 max_steps;「每步很快但方向乱兜圈」会先撞 max_iterations。两种失败的 failure_category_path 不同(v2_max_steps vs v2_max_iterations:1263/:1300),排障时看这个标签能快速定位是哪条预算爆的。

4.2 五个终态

所有收场都归到一组 mark_task_v2_as_*task_v2_service.py:1849+),它们都走同一个 _update_task_v2_status:1815)落库、记时长指标、发 webhook:

终态函数触发场景
mark_task_v2_as_completeduser_goal_achieved=true,总结后完成:1880
mark_task_v2_as_terminatedLLM 判定目标不可能(规划或完成校验任一):1924
mark_task_v2_as_failed无 task_type / 生成 block 失败 / 超步数 / 超轮数:1849
mark_task_v2_as_canceled工作流 run 被取消:1904
mark_task_v2_as_timed_out超时:1951

达成收尾走 _summarize_task_v2:2244)→ task_v2_summary prompt 综合整段 task_history 产出最终交付物;失败也尽力产一份「部分结果」(_best_effort_failure_deliverable:2217,永不抛异常,绝不干扰终态处理)。

4.3 navigate 的终端输出兜底(一个精细的容错)

navigate block 本该只导航,但有时底层 agent 的收尾 COMPLETE 动作里带了答案。主循环有段兜底:当 navigate 成功、但 extracted_information 为 null 时,去它的 COMPLETE 动作里捞回终端输出(_get_navigate_complete_output:2054)。

这里有两个刻意的容错边界:

  • 字符串输出超过 NAVIGATE_TERMINAL_OUTPUT_MAX_CHARS(2000)就截断(:2087)——因为 task_history 每轮都回灌 prompt,无界文本会撑爆上下文。
  • 结构化输出(dict/list)没法安全截断,超过 10 倍上限就整个丢弃:2101-2110),逼规划器另开一个 extract 任务,而不是塞一坨脏数据进历史。

5. 巧妙之处(可借鉴的技术)

  • 工作流「生长」而非「预定义」。 每轮把新 block 追加后整份重建工作流(:1035-1057),让「动态规划」复用了「静态工作流引擎」的全部执行能力——规划器不必自己实现执行,只管生成 block。这是 v2 复用 ch05 的核心手法。
  • 执行前规划 + 执行后复核 双 prompt。 task_v2.j2 管「下一步」、task_v2_check_completion.j2 管「收工没」,两次独立判断避免「以为做完了其实没做完」。
  • prompt 层面的反死循环工程。 「循环优先」和「最终答案在收尾自动合成、别用 task 去写总结」两条规则(task_v2.j2:6-13:46-49),直接从设计上堵死了最常见的两种「跑光预算」死法。这是把踩过的坑固化进 prompt 的典范。
  • 历史回灌 = 无状态循环的记忆。 每轮把 task_history 喂回 prompt,让本质无状态的 LLM 调用具备了「记得做过什么」的连续性,同时用字符/结构化上限守住上下文不膨胀。

6. 边界与局限

  • 强依赖 LLM 判断力。 拆解质量、是否用 loop、何时收工,全押在规划 prompt 的输出上。方向判断错了,就在预算内空转到失败。
  • 两条预算是硬墙。 复杂目标很容易撞 max_stepsmax_iterations(默认 50 轮)。prompt 里反复劝用 loop、别拿 task 写总结,本质都是在省预算。
  • 终止极度保守。 两个 prompt 都要求「必须有页面上明确的不可能证据」才 terminate,好处是不轻易放弃,代价是真正不可能的目标也可能兜到超预算才失败。
  • loop 依赖「循环值」抽得准。 _generate_loop_task 第一步的提取 block 若抓错/抓空那批值,整个循环就跑偏,且该失败在上层才被 mark(:1470-1476)。

7. 横向对比:task_v1 vs task_v2

同一个仓库里两代任务模型,取舍鲜明:

维度task_v1(单任务)task_v2(自主规划)
入口run_tasktask_v1_service.py:79run_task_v2task_v2_service.py:481
输入一个明确任务 + 目标 URL一句话大目标(URL 可省,LLM 推断)
循环固定单任务,交给 executor 跑底层 step 循环(AsyncExecutorFactory...execute_tasktask_v1_service.py:115动态外循环,每轮 LLM 现规划下一个 block(run_task_v2_helper:744
谁划任务边界用户规划器自己
产物执行一个任务边跑边「长」出一整个工作流
适用边界清晰的单步活需要多步拆解、事先画不出流程图的活

一句话: v1 是「照着你给的一张图走」,v2 是「你只给一句话,它边走边把图画出来」。v2 并不取代 v1——它在 v1/ch05 提供的感知、决策、执行、block 之上,只加了「LLM 动态生成下一个 block」这一层自治外循环。这就是 README 所谓「swarm of agents(一群 agent)」的工程真身(README.md:39)。

工程家族里 DOM 规划器与计算机使用(CUA)两种引擎的取舍见 04-engines-cua.md;task_v2 生成的 navigate block 底层可落到任一引擎执行。


8. 代码地图(导航索引)

主题文件符号
建空工作流壳skyvern/services/task_v2_service.pyinitialize_task_v2
推断起始 URL/标题skyvern/services/task_v2_service.pyinitialize_task_v2_metadata
外层入口 / 上下文 / 收尾skyvern/services/task_v2_service.pyrun_task_v2
核心主循环skyvern/services/task_v2_service.pyrun_task_v2_helper
轮数预算解析skyvern/services/task_v2_service.py_resolve_max_iterationsDEFAULT_MAX_ITERATIONS
物化导航 blockskyvern/services/task_v2_service.py_generate_navigation_task
物化提取 blockskyvern/services/task_v2_service.py_generate_extraction_task
物化循环 block(两次 LLM)skyvern/services/task_v2_service.py_generate_loop_task_generate_data_extraction_schema_for_loop
开局跳转 blockskyvern/services/task_v2_service.py_generate_goto_url_task
block 结果处理skyvern/services/task_v2_service.pyhandle_block_result
navigate 终端输出兜底skyvern/services/task_v2_service.py_get_navigate_complete_output
终态落库(×5)skyvern/services/task_v2_service.pymark_task_v2_as_completed/failed/terminated/canceled/timed_out
达成总结产出skyvern/services/task_v2_service.py_summarize_task_v2_best_effort_failure_deliverable
思考类型枚举skyvern/forge/sdk/schemas/task_v2.pyThoughtTypeThoughtScenario
规划 promptskyvern/forge/prompts/skyvern/task_v2.j2navigate/extract/loop 定义、循环优先、反死循环
完成校验 promptskyvern/forge/prompts/skyvern/task_v2_check_completion.j2required_subgoals、保守终止
循环内任务生成 promptskyvern/forge/prompts/skyvern/task_v2_generate_task_block.j2navigation_goal / data_extraction_goal / schema
提取 schema 生成 promptskyvern/forge/prompts/skyvern/task_v2_generate_extraction_task.j2schema
mini goal 模板skyvern/constants.pyMINI_GOAL_TEMPLATE
v1 单任务入口(对比)skyvern/services/task_v1_service.pyrun_task