<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Robotics on SniperPigeon笔记本</title><link>https://sniperpigeon.github.io/tags/robotics/</link><description>Recent content in Robotics on SniperPigeon笔记本</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Wed, 15 Apr 2026 15:35:00 +0800</lastBuildDate><atom:link href="https://sniperpigeon.github.io/tags/robotics/index.xml" rel="self" type="application/rss+xml"/><item><title>RoboSkiAgent: 用 LLM 驱动工业机器人</title><link>https://sniperpigeon.github.io/p/skiagent/</link><pubDate>Wed, 15 Apr 2026 15:35:00 +0800</pubDate><guid>https://sniperpigeon.github.io/p/skiagent/</guid><description>&lt;img src="https://sniperpigeon.github.io/p/skiagent/system_architecture.png" alt="Featured image of post RoboSkiAgent: 用 LLM 驱动工业机器人" /&gt;&lt;p&gt;这一个月来做的大部分工作零零散散有笔记但是是以英文存在的，加之大部分是在框架探索阶段的思考决策，感觉缺乏工程上的技术总结，于是便有想法先总结过去的经验catchup，不过量实在太大，先请Claude代劳。后面进入SFT阶段后还是改用手写。&lt;/p&gt;
&lt;h2 id="背景"&gt;背景
&lt;/h2&gt;&lt;p&gt;故事从一段实习开始。我加入时，项目已经到了末期——一条智能供应链的末端：自动导引车把零件从零件台送到装配台，UR10 机械臂完成最后的组装。CNC 加工、零件运输、机器人装配，三个子系统拼在一起。我的工作是负责机器人装配这一段。&lt;/p&gt;
&lt;p&gt;不过早先的 supervisor 也已离任，换来的是更加熟悉 AI 方向的新老板。此时项目基本要结题，但新老板提出可以以项目为背景继续探索：在这套系统上，LLM 能做什么？能做到什么程度？能不能实现真正的 Low-Code 机器人控制——操作员用自然语言描述任务，系统自动规划并执行？更进一步，有没有可能用这个场景生成训练数据，探索机器人专用 LLM 的 Fine-Tuning 路径，应用到未来的本地小模型上？&lt;/p&gt;
&lt;p&gt;RoboSkiAgent 就是在这个背景下开始的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一个约束驱动了整个架构"&gt;一个约束驱动了整个架构
&lt;/h2&gt;&lt;p&gt;在动手之前，先确定了一条设计约束：&lt;strong&gt;LLM 层永远不能出现坐标数值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;LLM是概率模型，无法从文字上做数学推理。让 LLM 说&amp;quot;把 &lt;code&gt;Part_A&lt;/code&gt; 放到 &lt;code&gt;Station_2&lt;/code&gt;&amp;ldquo;是合理的，让它说&amp;quot;在 x=312.5, y=-88.3 的位置上空100mm就位并放置&amp;quot;是强迫它做不擅长的事。坐标计算应该在底层完成，LLM 处理符号和意图。&lt;/p&gt;
&lt;p&gt;这条约束直接产生了一个分层问题：谁来算坐标？谁来理解意图？由此决定了 SkiLib 和 Agent 层的分离。&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="375px" data-flex-grow="156" height="4610" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://sniperpigeon.github.io/p/skiagent/system_architecture.png" srcset="https://sniperpigeon.github.io/p/skiagent/system_architecture_hu_587318113c5421dd.png 800w, https://sniperpigeon.github.io/p/skiagent/system_architecture_hu_7f414b7272c27585.png 1600w, https://sniperpigeon.github.io/p/skiagent/system_architecture_hu_49b007ae6f95e0d.png 2400w, https://sniperpigeon.github.io/p/skiagent/system_architecture.png 7213w" width="7213"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="skilib先把手脚造好"&gt;SkiLib：先把手脚造好
&lt;/h2&gt;&lt;p&gt;在接入任何 LLM 框架之前，先把机器人的&amp;quot;手脚&amp;quot;封装成一个纯 Python 库，&lt;strong&gt;完全没有 LangGraph 依赖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个选择不是偶然的，有两个具体动机：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可独立测试&lt;/strong&gt;：技能库可以单独 debug，Agent 框架只是调用它的客户，不会把测试环境和编排环境混在一起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;底层平台可迁移&lt;/strong&gt;：项目使用的是 RoboDK 仿真 + UR10，但后续有计划迁移到 Genesis——主要原因是 Genesis 更适合大规模并行仿真，可以用来生成 SFT 所需的大量 trajectory 数据。如果技能库和编排框架耦合在一起，换仿真平台就要同时改 Agent 逻辑；现在两者分离，换平台只需要重新实现 Primitive 层，Skill 逻辑和 Agent 编排完全不动。&lt;/p&gt;
&lt;h3 id="两层抽象primitive-vs-skill"&gt;两层抽象：Primitive vs Skill
&lt;/h3&gt;&lt;p&gt;技能库分成两层：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitive（原语）&lt;/strong&gt; 是最小的、平台相关的动作单元：&lt;code&gt;MoveJ&lt;/code&gt;（关节插值运动）、&lt;code&gt;MoveL&lt;/code&gt;（直线运动）、&lt;code&gt;Grasp&lt;/code&gt;、&lt;code&gt;Release&lt;/code&gt;。这一层可以 &lt;code&gt;import robodk&lt;/code&gt;，知道所有硬件细节，负责跟机器人打交道。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skill（技能）&lt;/strong&gt; 是平台无关的业务逻辑，&lt;strong&gt;禁止 &lt;code&gt;import robodk&lt;/code&gt;&lt;/strong&gt;，只依赖 &lt;code&gt;BasePrimitive&lt;/code&gt; 接口。&lt;code&gt;PickAndPlace&lt;/code&gt; 把 8 个步骤组合在一起：接近点 → 下降 → 抓取 → 提升 → 运输 → 下降 → 释放 → 撤离。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PickAndPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseSkill&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;REQUIRED_PRIMITIVES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;MoveJ&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;MoveL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Grasp&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Release&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pick_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;place_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SkillResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# pick_target 是符号名，方法内部解析为 RoboDK Item&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RobotContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pick_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pick_target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个分法在写测试时价值立刻体现出来：Skill 层的逻辑可以用 mock primitives 做单元测试，完全不需要启动仿真软件。&lt;/p&gt;
&lt;h3 id="skillresult一道防火墙"&gt;SkillResult：一道防火墙
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;SkillResult&lt;/code&gt; 是所有公开方法的统一返回类型，&lt;strong&gt;目的只有一个：LLM 永远看不到 Python traceback&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所有底层异常必须在 Primitive 层内部被捕获，翻译成结构化描述，才能往上传：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# LLM 看到的&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;phase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;PLANNING&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;error_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;IK_FAILURE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;No IK solution for Station_3 in current configuration.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;suggestion&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Try approaching from above.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;error_type&lt;/code&gt; 用的是字符串常量而不是枚举，原因很实际：不同 Primitive 有各自领域特有的错误类型，用枚举意味着每次加新 Primitive 都要改核心 &lt;code&gt;base.py&lt;/code&gt;。字符串常量在各自模块里定义，不侵入核心文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExecutionPhase&lt;/code&gt; 则用了枚举（VALIDATION / PLANNING / EXECUTION），因为这三个阶段对应 LLM 恢复时的三种决策分支，不会随 Primitive 扩展而变化——这两个选择背后的逻辑是一致的：&lt;strong&gt;稳定的抽象用枚举，容易变化的用字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="require_robot_active硬件锁"&gt;&lt;code&gt;@require_robot_active&lt;/code&gt;：硬件锁
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@require_robot_active&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SkillResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;系统挂起时（&lt;code&gt;halt_flag=True&lt;/code&gt;），任何带这个装饰器的调用都直接返回 &lt;code&gt;ERROR_ROBOT_INACTIVE&lt;/code&gt;，不触碰硬件。但这里有一个死锁风险：如果 &lt;code&gt;resume()&lt;/code&gt; 也被拦截，系统就永远无法恢复。所以有个白名单机制：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@require_robot_active&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bypass_halt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SkillResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;bypass_halt=True&lt;/code&gt; 必须显式声明，没有隐式例外。漏掉这个声明会导致系统永久卡死，这种错误在运行时才会暴露，所以在文档里单独标注了。&lt;/p&gt;
&lt;h3 id="tool_methods刻意不给-llm-的权限"&gt;TOOL_METHODS：刻意不给 LLM 的权限
&lt;/h3&gt;&lt;p&gt;Skill 暴露给 LLM 的方法在基类层面就做了限制：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;TOOL_METHODS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;check&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;try_execute&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# execute 故意不暴露&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;execute&lt;/code&gt; 会跳过 pre-flight 验证直接执行。LLM 不应该绕过校验——它要么用 &lt;code&gt;check&lt;/code&gt; 先探测一下，要么用 &lt;code&gt;try_execute&lt;/code&gt; 走完整流程。子类可以覆盖这个元组，但需要明确说明原因。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="agent-层plan-and-execute-状态机"&gt;Agent 层：Plan-and-Execute 状态机
&lt;/h2&gt;&lt;p&gt;整体是一个 LangGraph &lt;code&gt;StateGraph&lt;/code&gt;，五个主要节点：Supervisor → Planner → PlanReview → Dispatcher → Executor，加上两个 interrupt 处理节点。&lt;/p&gt;
&lt;p&gt;有一点值得先说清楚：&lt;code&gt;messages&lt;/code&gt; 字段在这个系统里&lt;strong&gt;最终不是通用事件总线&lt;/strong&gt;。最初的设计上，我们曾试图使用LangGraph的Reduce修饰器来对LLM产生的消息链进行修剪，去掉中间状态只留结论给后面的节点。不过后来发现我们选择了直接隔离各个节点。所以它只在 LLM 推理链上流转——Supervisor 读取初始指令，输出分析结果；Planner 读取 Supervisor 的最后一条输出；&lt;code&gt;replan&lt;/code&gt; 路径写入一条 &lt;code&gt;HumanMessage&lt;/code&gt; 触发重规划。节点之间的实际状态传递靠的是专用字段：&lt;code&gt;execution_log&lt;/code&gt;、&lt;code&gt;current_task&lt;/code&gt;、&lt;code&gt;halt_flag&lt;/code&gt;、&lt;code&gt;last_result&lt;/code&gt; 等。这个设计避免了 LLM 在每轮推理时看到越来越多的执行噪音。&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="213px" data-flex-grow="89" height="2048" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://sniperpigeon.github.io/p/skiagent/agent_graph.png" srcset="https://sniperpigeon.github.io/p/skiagent/agent_graph_hu_68d2ec0f46ed1334.png 800w, https://sniperpigeon.github.io/p/skiagent/agent_graph_hu_b9ec0d01d2887a88.png 1600w, https://sniperpigeon.github.io/p/skiagent/agent_graph.png 1823w" width="1823"&gt;&lt;/p&gt;
&lt;p&gt;graph设计上有两个节点分别处理人类介入，不过为了区分人类操作和异常处理的Human-In-The-Loop (HITL)，将他们分开作为两个节点，也减少不必要的条件判断和复杂性。&lt;/p&gt;
&lt;h3 id="supervisor只做情报收集"&gt;Supervisor：只做情报收集
&lt;/h3&gt;&lt;p&gt;Supervisor 不规划，不执行，只做一件事：把自然语言指令转化成&amp;quot;知识饱和&amp;quot;的符号描述。&lt;/p&gt;
&lt;p&gt;它有一套只读工具（T-skills）：&lt;code&gt;list_targets()&lt;/code&gt;、&lt;code&gt;list_objects()&lt;/code&gt;、&lt;code&gt;check_item_exists()&lt;/code&gt; 等，全部只查询 RoboDK 场景，不做任何动作。输出是结构化的 &lt;code&gt;SupervisorOutput&lt;/code&gt;：场景里有哪些目标点、哪些工件、哪些工具，全部用符号名表示。&lt;/p&gt;
&lt;p&gt;可用技能列表由代码注入 system prompt，LLM 不需要记住，也不需要填写——这个信息是运行时动态生成的。&lt;/p&gt;
&lt;h3 id="planner用工具调用代替结构化-json-输出"&gt;Planner：用工具调用代替结构化 JSON 输出
&lt;/h3&gt;&lt;p&gt;Planner 有一个关键的设计演进。最初让 LLM 直接输出 &lt;code&gt;todo_list&lt;/code&gt; JSON，问题是 LLM 需要记住每个 Skill 的参数格式，弱模型很容易输出不合法的结构。&lt;/p&gt;
&lt;p&gt;改成工具调用方式：为每个注册的 Skill 动态生成一个 &lt;code&gt;add_&amp;lt;SkillName&amp;gt;_task&lt;/code&gt; 工具，&lt;strong&gt;复用 &lt;code&gt;try_execute&lt;/code&gt; 的 args_schema&lt;/strong&gt;。LLM 不需要知道 JSON 结构，只需要调用工具；Pydantic 自动验证参数；&lt;code&gt;task_id&lt;/code&gt; 由代码自动分配，不会出现编号重复或跳号的问题。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make_planner_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;skill_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_skills&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;skill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_skill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skill_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 复用 try_execute 的参数 schema，参数校验自动完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;try_exec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;_try_execute&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StructuredTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;add_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;skill_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_task&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;args_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;try_exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="planreview结构性强制审批"&gt;PlanReview：结构性强制审批
&lt;/h3&gt;&lt;p&gt;这是一个 LangGraph &lt;code&gt;interrupt&lt;/code&gt;，图结构保证每次 Planner 完成后必经此节点。&lt;/p&gt;
&lt;p&gt;之前尝试过在 prompt 里要求 LLM &amp;ldquo;在计划前插入一个 manual task 让操作员审批&amp;rdquo;。这在弱模型上不可靠——有时会忘记，有时位置不对。&lt;strong&gt;审批这件事不应该依赖 LLM 的执行意愿，而应该由图结构强制保证&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;replan&lt;/code&gt; 路径把操作员反馈直接写入 &lt;code&gt;messages&lt;/code&gt; 送回 Supervisor，比 abort + 重新输入指令效率高得多：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;replan&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;return_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;messages&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Please replan: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;return_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;todo_list&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="dispatcher执行槽语义"&gt;Dispatcher：执行槽语义
&lt;/h3&gt;&lt;p&gt;Dispatcher 是纯代码节点，设计了一个&amp;quot;执行槽&amp;quot;语义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;current_task == {}&lt;/code&gt; → 槽空闲，pop 下一个任务填入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current_task != {}&lt;/code&gt; → 槽被占用，跳过不覆盖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任务失败时，&lt;code&gt;current_task&lt;/code&gt; 保留原始任务。操作员选 retry 后，Executor 拿到完整信息重试，不需要任何额外传递。这个&amp;quot;单一真相来源&amp;quot;的设计替代了之前用 &lt;code&gt;last_result&lt;/code&gt; 作为隐式路由信号的方案，语义更清晰。&lt;/p&gt;
&lt;h3 id="executor双层恢复--一个有意思的升级机制"&gt;Executor：双层恢复 + 一个有意思的升级机制
&lt;/h3&gt;&lt;p&gt;先直接调 &lt;code&gt;skill.try_execute()&lt;/code&gt;。失败了，启动 LLM 恢复循环，给它工具：这个 Skill 的 &lt;code&gt;check/try_execute&lt;/code&gt;、&lt;code&gt;list_targets&lt;/code&gt;，还有一个特殊工具 &lt;code&gt;escalate_to_hitl&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;escalate_to_hitl&lt;/code&gt; 的实现值得单独说：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_escalate_to_hitl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;_EscalateHITLException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;escalate_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StructuredTool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_escalate_to_hitl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle_tool_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 关键：让异常真正穿透&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;LLM 调这个工具时，实际上是在触发一个 Python 异常，被 Executor 节点的 &lt;code&gt;try/except&lt;/code&gt; 捕获。&lt;code&gt;handle_tool_error=False&lt;/code&gt; 是关键——LangChain 默认会把工具异常包成错误消息返回给 LLM，这里需要让它真正穿透出去。&lt;/p&gt;
&lt;p&gt;这样 LLM 有两条路：继续尝试其他参数，或者调 &lt;code&gt;escalate_to_hitl&lt;/code&gt; 放弃交人工。选哪条完全由 LLM 判断，Executor 不做任何隐式决策。&lt;/p&gt;
&lt;h3 id="hitl-拆分用结构消除非法操作组合"&gt;HITL 拆分：用结构消除非法操作组合
&lt;/h3&gt;&lt;p&gt;最初一个 &lt;code&gt;HumanIntervention&lt;/code&gt; 节点处理两种入口：任务执行失败（&lt;code&gt;TASK_FAILURE&lt;/code&gt;）和计划内人工步骤（&lt;code&gt;MANUAL_TASK&lt;/code&gt;）。两种入口的合法 actions 不同——失败时可以 retry，但人工步骤 retry 毫无意义（机器人根本不会执行人工任务）。靠运行时 guard 防御这个非法组合，是一个设计异味。&lt;/p&gt;
&lt;p&gt;拆成两个独立节点后，非法组合从结构上消失：&lt;code&gt;manual_intervention_handler&lt;/code&gt; 只提供 &lt;code&gt;complete/abort&lt;/code&gt;，&lt;code&gt;hitl_handler&lt;/code&gt; 只提供 &lt;code&gt;retry/next_task/replan/abort&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hitl_handler&lt;/code&gt; 里的 &lt;code&gt;replan&lt;/code&gt; 路径是后来加的：某个任务彻底失败，操作员认为不是执行问题，而是整个规划方向有问题，这时可以直接触发重规划，不需要 abort 后重新输入指令。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="真正有意思的问题llm-和环境怎么交互"&gt;真正有意思的问题：LLM 和环境怎么交互？
&lt;/h2&gt;&lt;p&gt;做这个系统的过程中，真正让人深思的问题不是怎么连 RoboDK API，而是：&lt;strong&gt;LLM 应该以什么粒度、什么方式跟机器人环境交互？这个设计选择直接决定了训练数据的形态和 SFT 的可行性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以列出几种范式及其权衡：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitive+Skill 双层抽象&lt;/strong&gt;（当前 V1）：确定性强，行为可预测，接近 PDDL 规划器的抽象方式。问题是 LLM 在这里基本只做参数填写，Skill 层的复杂逻辑是硬编码的，LLM 的能力没有被真正测试，生成的 trajectory 对 SFT 价值有限。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitive 完全 ReAct Loop&lt;/strong&gt;：LLM 直接调用每一个原语动作，中间每步都观测环境状态。trajectory 很长，真正考验指令遵循能力，但极度低效，充满冗余的感知-决策-执行循环——而且很多冗余步骤（比如碰撞检测）完全可以用确定性代码处理，没必要浪费推理次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code-as-Policy&lt;/strong&gt;：LLM 一次性生成完整的执行代码，代码里可以包含确定性的逻辑分支和异常处理，生成的代码还有复用价值。但一旦运行出错，需要把错误传回 LLM 重新生成，没有 ReAct 式的细粒度恢复机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;逻辑分组&lt;/strong&gt;（一个折中方向）：LLM 一次性生成一段&amp;quot;不需要逻辑硬分叉&amp;quot;的动作序列，中间只做异常捕获，真正需要决策的错误才抛给 LLM。这样既减少了推理次数，又保留了 LLM 在关键节点的决策能力。&lt;/p&gt;
&lt;p&gt;这四种范式在效率、trajectory 长度、LLM 能力利用率、SFT 数据质量上各有取舍，没有最优解，选哪种取决于研究目标。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="v2去掉-python-skill-层走向-low-code"&gt;V2：去掉 Python Skill 层，走向 Low-Code
&lt;/h2&gt;&lt;p&gt;V1 的双层 Python 抽象在工程交付场景下是合理的，但对于当前的研究目标来说是过度设计——UR10 在这个项目里需要做的动作并不复杂，维护两层类的成本高，LLM 的能力也没有被真正测试。&lt;/p&gt;
&lt;p&gt;V2 的核心改动是：&lt;strong&gt;把 Skill 的 Python 编码换成自然语言描述的 &lt;code&gt;skill.md&lt;/code&gt; 文件，Executor 直接调用 Primitive。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个改动服务于两个目标：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正的 Low-Code&lt;/strong&gt;：新增一个技能不需要写 Python 类，只需要写一个描述文件，告诉 LLM 这个技能应该按什么顺序调用哪些 Primitive，需要注意什么边界条件。理想状态下，LLM 读取 &lt;code&gt;skill.md&lt;/code&gt; 后可以自主组合 Primitive 完成任务，甚至通过观察执行轨迹归纳出新的 skill 描述——相当于让 LLM 自己写说明书。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更长、更有价值的 trajectory&lt;/strong&gt;：V2 让 LLM 直接面对 Primitive 级别的决策，trajectory 更长，更能反映模型的指令遵循能力。配合 Genesis 仿真的大规模并行能力，可以收集到更有价值的 agentic 训练数据。&lt;/p&gt;
&lt;p&gt;为了支持 V2 的感知决策，还在规划增加 &lt;strong&gt;Perceptron 层&lt;/strong&gt;——传感器接口，让 LLM 能查询&amp;quot;夹爪现在是否抓住了东西&amp;rdquo;、&amp;ldquo;零件是否到位&amp;quot;等状态，而不只是盲目执行运动序列。这样 &lt;code&gt;skill.md&lt;/code&gt; 里可以自然地描述检测步骤，LLM 也有机会在执行中途做判断，例如执行一次抓取后立即检测是否成功，而不是等整个序列跑完才发现失败。&lt;/p&gt;
&lt;p&gt;一次推理生成完整执行序列（类似 Code-as-Policy 的思路）也在考虑范围内：可以测试模型对 &lt;code&gt;skill.md&lt;/code&gt; 的遵循程度，减少推理轮次，代价是减少了中途的环境感知机会。这个 tradeoff 需要在实验中评估。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="当前的技术债"&gt;当前的技术债
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;LLM 恢复结果不透明&lt;/strong&gt;：Executor 的 LLM 恢复循环成功时，现在靠&amp;quot;没有抛 escalation 异常&amp;quot;来判断成功，并构造一个 &lt;code&gt;SkillResult(success=True, message=&amp;quot;Recovered by LLM retry.&amp;quot;)&lt;/code&gt;。实际执行结果没有被真正捕获。正确做法是 intercept 工具调用，把实际的 &lt;code&gt;SkillResult&lt;/code&gt; 写回来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规划历史的消息累积&lt;/strong&gt;：Supervisor 和 Planner 每轮产生的消息会写入 &lt;code&gt;messages&lt;/code&gt;。&lt;code&gt;replan&lt;/code&gt; 路径触发重规划时，历史消息还在，下一轮 Supervisor 会看到之前的执行上下文——这在某些场景下是有用的，但也可能引入噪音。&lt;code&gt;RemoveMessage&lt;/code&gt; 清理的 ID 范围需要精确设计，实现还没完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Genesis 迁移&lt;/strong&gt;：当前仿真环境是项目遗留的 RoboDK + UR10，后续迁移到 Genesis 主要是为了大规模 trajectory 生成。SkiLib 的分层设计让这个迁移只需要重新实现 Primitive 层，但具体的 API 映射工作还没开始。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这个系统从一个实习项目的工程实现出发，走到了一个关于&amp;quot;LLM 应该怎样学会操控机器人&amp;quot;的研究问题上。两层 Python 抽象适合工程交付，但对于研究而言，更有价值的可能恰恰是让 LLM 直面更复杂的决策情境——怎么设计这个情境，怎么收集有价值的 trajectory，是接下来要认真对待的问题。&lt;/p&gt;</description></item></channel></rss>