<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Sia&apos;s Space</title><description>记录技术、项目与持续发生的思考</description><link>https://siaspace.vercel.app</link><item><title>装进 Agent 的 Skill，后来变成了什么？</title><link>https://siaspace.vercel.app/blog/agent-capability-software-supply-chain</link><guid isPermaLink="true">https://siaspace.vercel.app/blog/agent-capability-software-supply-chain</guid><description>一份 Skill 开始被分享、更新和组合之后，怎样知道 Agent 实际用的是哪份内容？</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果 Skill 只是一份自己维护的 Markdown，管理起来很直观：改完试一下，不合适就用 Git 回退。&lt;/p&gt;
&lt;p&gt;但把它分享出去以后，情况会慢慢变复杂。别人可能给它接了一个工具，又引用另一个 Skill；上游更新时，依赖也跟着变了。文件名还叫原来的名字，Agent 拿到的内容却未必是昨天那一份。&lt;/p&gt;
&lt;p&gt;我觉得，从这一步开始，就可以借用软件包管理的思路了。版本、依赖锁定、测试记录，这些看起来有点繁琐的东西，都是为了以后能回答一个简单的问题：这次究竟跑了什么？&lt;/p&gt;
&lt;p&gt;下面用 &lt;strong&gt;Capability&lt;/strong&gt; 表示一组可供 Agent 使用的指令、工具和资源，Skill 是它的一种封装。这里讨论的是设计思路，不是某个已经完整实现的平台。&lt;/p&gt;
&lt;h2&gt;安装按钮背后，还有哪些东西&lt;/h2&gt;
&lt;p&gt;一个“生成报告”的 Skill，可能会调用搜索工具、解析文件，再把结果发布出去：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;report-generator
├── web-research       → network:read
├── document-parser    → files:read
└── publisher          → files:write, external:publish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户看到的是一个功能名称，真正进入运行环境的却是一组依赖。只记下“安装了 report-generator”，排查问题时就会缺少很多信息：搜索工具是什么版本？发布服务有没有换过？这次使用的指令，和审核时看到的是否相同？&lt;/p&gt;
&lt;p&gt;普通包管理器会留下 lockfile，原因也类似。版本范围表达的是“可以选哪些”，锁定记录表达的是“这一次选中了谁”。放到 Agent 里，还需要记住工具实现、权限配置，以及能够观察到的模型版本。&lt;/p&gt;
&lt;p&gt;不一定一开始就做很重的管理系统。哪怕只是个人项目，把实际加载的内容和解析结果保存下来，也比事后猜测强。&lt;/p&gt;
&lt;h2&gt;先把发布的那份内容固定下来&lt;/h2&gt;
&lt;p&gt;我会先为一份 Skill 区分三样东西：说明要求的 Manifest、固定内容的 Snapshot，以及记录实际依赖选择的 Lockfile。它们的具体格式可以继续调整，但职责最好别混在一起。&lt;/p&gt;
&lt;p&gt;比如 Manifest 可以描述这些内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Manifest
├── instructions
├── input / output contract
├── required tools
├── dependency constraints
├── requested permissions
├── model / runtime assumptions
├── evaluation cases
└── provenance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里既有“怎么做”，也有“需要什么”和“在哪些条件下试过”。如果只有操作步骤，安装的人就很难判断它需要访问什么资源，是否适合自己的运行环境。&lt;/p&gt;
&lt;p&gt;快照则要把相关文件固定下来。审核通过后继续原地修改脚本，等于把审核过的东西换掉了；顶层版本号没变，也不能让旧结论自动适用于新内容。&lt;/p&gt;
&lt;p&gt;所以测试、审核和发布最好都指向同一份明确的内容。评测记录可以单独存放，但需要绑定它测过的快照和运行环境。&lt;a href=&quot;/notes/capability-manifest-snapshot-lockfile&quot;&gt;同名 Skill 还是昨天那一份吗？&lt;/a&gt;里整理了一个可能的实现方式。&lt;/p&gt;
&lt;h2&gt;依赖多了，权限也会跟着变化&lt;/h2&gt;
&lt;p&gt;前面那个报告 Skill，如果同时能读工作区和向外发布，那么它就可能把读到的内容发送出去。两个工具分别看都正常，组合起来却多了一条数据流。&lt;/p&gt;
&lt;p&gt;因此我会同时看两件事：加载了哪些依赖，以及这些依赖在当前授权下能够访问什么、修改什么。两者相关，但不能直接画等号。一个依赖可能只在测试时使用，同一个工具也可能在不同任务里拿到不同范围的权限。&lt;/p&gt;
&lt;p&gt;子能力通常只需要完成当前调用所需的那部分授权。把父任务的整个上下文和凭证一股脑传下去，写起来省事，之后就很难判断权限到底扩到了哪里。&lt;/p&gt;
&lt;p&gt;安装时可以分析潜在组合，执行时仍要按真实参数检查资源。文件路径和发布目标经常要到调用时才知道，不能只靠安装页面上的几枚权限标签。&lt;a href=&quot;/notes/capability-authority-closure-composition-risk&quot;&gt;权限组合笔记&lt;/a&gt;主要讨论的就是这个问题。&lt;/p&gt;
&lt;h2&gt;“测试通过”还需要补上后半句话&lt;/h2&gt;
&lt;p&gt;一个 Skill 跑通了一次，可以说明这次任务完成了。要据此决定是否发布，还得知道用的什么模型、工具和初始状态，以及换一组输入以后会怎样。&lt;/p&gt;
&lt;p&gt;我希望测试记录能补完整这句话：哪份内容，在什么环境里，对哪些样本，按什么方法，得到了什么结果。&lt;/p&gt;
&lt;p&gt;测试也不能只看最终答案写得好不好。报告内容可能正确，中间却覆盖了别人的文件；任务可能完成了，但重试导致同一条消息发了两遍。任务质量、状态变化和权限行为需要分别检查，不能让一个总分把问题平均掉。&lt;/p&gt;
&lt;p&gt;τ-bench 和 ToolSandbox 都是这方面可以继续读的材料。我把评测对象、重复运行和发布证据的关系放在了&lt;a href=&quot;/notes/capability-evidence-review-progressive-delivery&quot;&gt;另一篇笔记&lt;/a&gt;。其中一个对我很有用的提醒是：测试结论也会过期。内容没有变化，远程模型或工具环境仍然可能变。&lt;/p&gt;
&lt;h2&gt;更新先被发现，再决定何时使用&lt;/h2&gt;
&lt;p&gt;开发时让所有地方都跟随 &lt;code&gt;latest&lt;/code&gt; 很方便，上游一改，马上就能试到。但如果每次运行都重新解析最新依赖，一次失败就很难回到当时的环境，小范围验证更新也无从谈起。&lt;/p&gt;
&lt;p&gt;我更倾向于把发现更新和启用更新分开：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Registry publishes v1.4
        ↓
Consumer detects compatible candidate
        ↓
Resolve dependency and authority changes
        ↓
Run evaluation / canary
        ↓
Explicitly activate immutable snapshot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;个人项目可以简化审核环节，但仍然值得保留旧的解析记录。出问题时，至少知道要退回哪一组内容，而不是只把最外层的版本号改回去。&lt;/p&gt;
&lt;p&gt;审核时也不必从头读完所有文件。先看相对上次批准版本新增了什么依赖、工具、外部地址和权限，通常更容易找到值得仔细检查的地方。提示词只改了两行，背后新增了一个可执行脚本，影响可能比大段措辞调整更大。&lt;/p&gt;
&lt;h2&gt;撤销以后，旧任务怎么办&lt;/h2&gt;
&lt;p&gt;如果某个版本有问题，阻止新的安装只是第一步。还需要查出哪些运行加载了它、是否实际调用过相关能力，以及已经生成了哪些文件或外部产物。&lt;/p&gt;
&lt;p&gt;直接把仓库里的那份内容删掉，会让历史记录失去参照。我会保留必要的版本身份和撤销原因，再按影响范围决定停止、隔离还是迁移已有任务。至于它已经发出去的消息、写入的文件，要另行处理，不会随着版本回滚自动消失。&lt;/p&gt;
&lt;p&gt;这也是管理发布和管理执行需要互相配合的地方：发布系统告诉运行时哪些版本可以用，运行时记录它们实际做了什么。两边用快照和解析记录关联起来，撤销才不只是修改一个状态字段。&lt;/p&gt;
&lt;h2&gt;一个徽章能说明多少&lt;/h2&gt;
&lt;p&gt;签名、测试和审核都很有用，但各自能说明的事情有限。签名可以帮助确认内容来自哪个密钥，不能保证作者没有犯错；一次审核对应的是当时那份内容，也不覆盖未来版本。下载量多更不能直接当成安全结论。&lt;/p&gt;
&lt;p&gt;所以我不太想把所有判断压成一个“可信”标签。把来源、版本、测试条件和权限范围摊开，安装的人反而更容易决定：这份 Skill 能放到什么任务里使用，哪些地方还需要隔离和观察。&lt;/p&gt;
&lt;p&gt;Skill 的吸引力，是让一个人的工作方法更容易被别人复用。我希望这种方便能保留下来，同时在更新或失败时，还能沿着记录找到具体发生了什么。对我来说，这就是把它当作软件依赖管理的理由。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Mavroudis et al., &lt;a href=&quot;https://arxiv.org/abs/2607.01136&quot;&gt;Skills Are Not Islands: A Systems Approach to Agentic Skill Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sierra Research, &lt;a href=&quot;https://arxiv.org/abs/2406.12045&quot;&gt;τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apple, &lt;a href=&quot;https://arxiv.org/abs/2408.04682&quot;&gt;ToolSandbox: A Stateful, Conversational, Interactive Evaluation Benchmark for LLM Tool Use Capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenSSF, &lt;a href=&quot;https://slsa.dev/&quot;&gt;SLSA — Supply-chain Levels for Software Artifacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CISA, &lt;a href=&quot;https://www.cisa.gov/sbom&quot;&gt;Software Bill of Materials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The Update Framework, &lt;a href=&quot;https://theupdateframework.github.io/specification/latest/&quot;&gt;TUF Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NIST, &lt;a href=&quot;https://csrc.nist.gov/Projects/ssdf&quot;&gt;Secure Software Development Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Agent 出了错，为什么翻聊天记录还不够？</title><link>https://siaspace.vercel.app/blog/agent-trajectories-as-provenance-graphs</link><guid isPermaLink="true">https://siaspace.vercel.app/blog/agent-trajectories-as-provenance-graphs</guid><description>从一份报告追到它用过的资料、工具和授权：聊聊 Agent 执行记录里容易丢失的关系。</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;假设 Agent 生成了一份有问题的报告。打开聊天记录，能看到它搜过资料、读过文件，也调用过几个工具。但报告里的那句话来自哪份资料？写入时用了哪个版本？按时间排好的消息，未必能直接回答。&lt;/p&gt;
&lt;p&gt;并行任务会让问题更明显。先返回的结果不一定被使用，子任务写出的草稿也不一定被采纳。只把消息接成一长串，很容易把“发生在前面”看成“导致了后面”。&lt;/p&gt;
&lt;p&gt;我想补上的，是这些可以核对的联系：一次调用收到了哪些输入，生成了什么结果，又经过什么授权改变了外部状态。这样的来源追溯，通常叫 &lt;strong&gt;Provenance&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这篇沿着那份报告往回看，试着把需要记录的东西串起来。讨论的都是系统能观察到的事件，不涉及模型内部隐藏的思维过程。&lt;/p&gt;
&lt;h2&gt;日志已经很多了，还缺什么&lt;/h2&gt;
&lt;p&gt;聊天记录适合回看交互，调用链适合查耗时和失败位置。比如一份 Trace 可以清楚地展示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;turn span
├── model call  2.1s
├── tool call   0.8s
└── model call  1.7s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果工具读了三份文档，模型请求最后只放进一份，普通的耗时记录不会自动替我们标出这件事。需要在组织输入的地方，明确记录“这次调用收到了哪份数据”。&lt;/p&gt;
&lt;p&gt;报告的形成过程也很容易分叉。搜索和文件读取可以并行，另一个子任务又交来一份草稿：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    ┌→ web search ─→ source A ─┐
user request ─→ plan                           ├→ synthesis ─→ report
                    └→ file read  ─→ source B ─┘

plan ─→ sub-agent ─→ draft ────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这些联系保存成图，方便之处在于可以从结果往回查，也可以从某份有问题的输入向后查。时间线仍然有用，只是它回答的是另一类问题。&lt;/p&gt;
&lt;h2&gt;先用三个概念把事情分开&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/prov-overview/&quot;&gt;W3C PROV&lt;/a&gt;里的三个核心概念，可以拿来做起点：Entity 表示某份数据或状态，Activity 表示一次活动，Agent 表示为这次活动负责的主体。&lt;/p&gt;
&lt;p&gt;放到一个写文件的例子里，旧文件和新文件是两份 Entity，写入是 Activity，执行者可能是代表用户行动的服务账户。这里的 Agent 不只指大模型。&lt;/p&gt;
&lt;p&gt;粒度不用细到每个 token。先记录输入快照、模型响应、工具调用、授权决定和产物版本，就已经能把很多原本挤在一行日志里的事拆开。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;file:/project/config.json@sha256:…
database:record/42@version:17
http-response@etag:…
capability@sha256:…
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这些引用带了版本信息，是因为路径本身会变化。今天的 &lt;code&gt;config.json&lt;/code&gt;，不一定还是昨天读过的那份。如果外部服务无法给出固定版本，就留下可获得的 ETag、请求标识和观测时间，并明确说明哪些内容没能固定。&lt;/p&gt;
&lt;h2&gt;放进上下文，不等于真的支撑了结论&lt;/h2&gt;
&lt;p&gt;假设搜索 A 先返回，文件 B 后返回，然后模型写出报告。仅凭时间顺序，不能断定报告同时使用了 A 和 B。A 可能已经被丢弃，实际请求里只有 B。&lt;/p&gt;
&lt;p&gt;因此，输入关系应该由构造模型请求的代码来记录，而不是让模型事后回忆。记录可以像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;model-call-3 used:
  message:user-1
  tool-result:file-B
  capability-resolution:72cd…
  policy-profile:b519…
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使记录了 B 进入上下文，也不能据此说模型使用了 B 的每句话。模型自己给出的引用、系统观察到的输入、验证器确认的派生关系，证据强度不一样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;observed input      系统确认数据进入活动
declared citation   模型或组件声明使用了来源
verified derivation 确定性转换或验证器确认关系
inferred relation   分析器根据行为推断
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我觉得这一步很重要。一张图画得再完整，如果所有边都混成同一种“因为”，它仍然可能把猜测包装成事实。事件格式和关系证据的细节，放在&lt;a href=&quot;/notes/agent-provenance-event-model-identity-causality&quot;&gt;这篇笔记&lt;/a&gt;里。&lt;/p&gt;
&lt;h2&gt;请求、获准和真正写入，要分别记录&lt;/h2&gt;
&lt;p&gt;模型提出 &lt;code&gt;write_file&lt;/code&gt;，权限检查返回允许，远程服务完成写入，这其实是三个不同的时刻：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Candidate Action
    ↓ authorization
Authorized Invocation
    ↓ execution
State Transition
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中间任何一步都可能失败。模型提了请求，不代表系统允许；系统允许了，远程调用也可能超时；超时又不一定意味着外部完全没发生变化。&lt;/p&gt;
&lt;p&gt;所以我希望记录里能找到候选调用、授权决定、实际执行尝试，以及写入前后的状态版本。只看到一句 &lt;code&gt;write_file succeeded&lt;/code&gt;，仍然不知道谁批准了它，批准的是哪个对象，重试有没有重复产生结果。&lt;/p&gt;
&lt;p&gt;Skill 的版本也应该跟进来。某份内容在发布时有测试记录，运行时却只记了名字，两边就断开了。把快照、依赖解析结果、工具绑定和策略版本关联到当前任务，之后才查得到某次更新影响了哪些执行。&lt;/p&gt;
&lt;h2&gt;子任务完成了，父任务用了吗&lt;/h2&gt;
&lt;p&gt;子 Agent 交回来一份草稿，不等于主任务已经采用它。父任务可能认为它过期、内容不合适，或者在它完成前就被用户取消了。&lt;/p&gt;
&lt;p&gt;因此，除了父子关系，我还想记录委托时给了哪些输入和权限、任务属于哪一轮，以及返回结果有没有被采纳：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sub-agent draft
      ↓ used
[adoption decision]
      ↓ generated
accepted evidence
      ↓ used
[final synthesis]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“产出了”和“被使用了”分开以后，排查也更有针对性。旧子任务返回了结果，但系统没有让它进入新一轮上下文，这与旧结果被直接写入最终报告，显然是两回事。&lt;/p&gt;
&lt;p&gt;同样，任务取消和实际提交的关系需要通过任务版本、授权失效和因果记录来核对，不能只比较两台机器日志上的时间戳。&lt;/p&gt;
&lt;h2&gt;为了排查，把所有内容永久存下来？&lt;/h2&gt;
&lt;p&gt;我不太愿意这么做。模型请求和工具响应里可能有用户文件、访问凭证或内部资源名称。保存得越完整，访问范围和删除问题就越需要认真处理。&lt;/p&gt;
&lt;p&gt;一个比较容易继续讨论的方案，是把图里的关系和实际正文分开。关系保存必要的身份、类型和引用；正文采用独立的访问权限和保留期限。密钥之类的内容则应该尽量在进入日志之前排除。&lt;/p&gt;
&lt;p&gt;正文到期以后，可以留下“这里曾经有一份输入、现在已经不可用”的标记，让下游关系仍然能解释。但这个标记不能再证明原来的具体内容，查询时也要如实展示缺口。&lt;/p&gt;
&lt;p&gt;Hash 和签名同样有边界。它们能帮助发现某些修改，却不能证明记录器从未漏记，也不能保证外部服务说的每句话都是真的。低熵敏感值即使只留下摘要，也可能被猜出来，不能把 Hash 当成通用的匿名化方法。&lt;/p&gt;
&lt;p&gt;更细的存储、校验和删除问题，整理在&lt;a href=&quot;/notes/trajectory-integrity-redaction-retention&quot;&gt;日志保留笔记&lt;/a&gt;里。&lt;/p&gt;
&lt;h2&gt;重放时，先说清楚想重放什么&lt;/h2&gt;
&lt;p&gt;看一遍旧记录，用固定工具响应测试新代码，再次调用真实模型和工具，这几种行为经常都叫“重放”。但它们的用途和后果不同。&lt;/p&gt;
&lt;p&gt;来源图能帮助找回当时的输入与执行关系。要重复实验，还需要固定环境、工具状态和调度条件；如果重新访问外部服务，就可能读到新内容，也可能产生新的写入。即使参数一样，远程模型也未必逐字返回相同答案。&lt;/p&gt;
&lt;p&gt;所以我更想比较的是关键行为：有没有越权、有没有重复提交、使用了哪些输入、最终状态是否满足约束。把这些和文本差异一起看，比只盯着最终回答更有帮助。&lt;a href=&quot;/notes/trajectory-replay-incident-impact-analysis&quot;&gt;重放与事故分析笔记&lt;/a&gt;沿着这个方向继续展开。&lt;/p&gt;
&lt;h2&gt;最后还是回到那份报告&lt;/h2&gt;
&lt;p&gt;有了这些记录，排查时可以从报告找到实际输入，从写入找到授权，从某个被撤销的 Skill 版本找到仍然存在的产物。答案不一定立刻出现，但下一步要查什么会清楚很多。&lt;/p&gt;
&lt;p&gt;我也不希望为了“可追溯”先搭一张包罗万象的大图。可以先选几个确实会问的问题，检查现有事件能不能回答，再补缺失的关系。比如：这次写入是谁允许的？子任务的哪份结果进入了报告？哪份输入已经到期删除了？&lt;/p&gt;
&lt;p&gt;对我来说，好的执行记录应该能支撑这样的追问。它不替我们做最后的因果判断，但可以让判断有材料可查，也让那些暂时不知道的地方明明白白地留在记录里。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;W3C, &lt;a href=&quot;https://www.w3.org/TR/prov-overview/&quot;&gt;PROV Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;W3C, &lt;a href=&quot;https://www.w3.org/TR/prov-dm/&quot;&gt;PROV-DM: The PROV Data Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenTelemetry, &lt;a href=&quot;https://opentelemetry.io/docs/concepts/signals/traces/&quot;&gt;Traces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;in-toto, &lt;a href=&quot;https://github.com/in-toto/attestation&quot;&gt;Attestation Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SLSA, &lt;a href=&quot;https://slsa.dev/provenance/&quot;&gt;Provenance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AgentTrails, &lt;a href=&quot;https://arxiv.org/abs/2607.18816&quot;&gt;A Framework for Execution-Trace-Based Evaluation of LLM Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anthropic, &lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot;&gt;Building Effective Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Agent 可以自己做主到哪一步？</title><link>https://siaspace.vercel.app/blog/autonomy-is-a-budget</link><guid isPermaLink="true">https://siaspace.vercel.app/blog/autonomy-is-a-budget</guid><description>从并行工具、子任务取消和文件权限聊起：怎样给 Agent 留出探索空间，又让执行结果可控。</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;给 Agent 接上工具之后，很容易想继续往前加：多开几个任务，多给一点权限，让它自己决定接下来做什么。看着它一口气完成一长串操作，确实很有吸引力。&lt;/p&gt;
&lt;p&gt;不过，有几个问题让我更在意它背后的执行代码。两个工具同时修改一份配置，结果会不会互相覆盖？用户点了取消，已经启动的子任务怎么办？模型把文件路径填对了，这次读取就一定被允许吗？&lt;/p&gt;
&lt;p&gt;这篇想聊的就是这些看起来不太起眼的地方。我把管理工具调度、状态、权限和任务生命周期的那层代码叫作 &lt;strong&gt;Execution Harness&lt;/strong&gt;。名字可以换，事情总要有人做。&lt;/p&gt;
&lt;h2&gt;先看一个很容易写出来的并行调用&lt;/h2&gt;
&lt;p&gt;模型一次返回多个工具请求，最直接的处理方式大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;results = await asyncio.gather(
    *[execute_tool(call) for call in tool_calls]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果几个请求分别查询互不相关的资料，这样做通常能省下不少等待时间。但把搜索换成配置更新，问题就来了。&lt;/p&gt;
&lt;p&gt;假设两个工具分别修改名称和描述。它们都先读取整份配置，在本地改一个字段，再把整份配置写回去：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def update_name():
    config = await get_config()
    config[&quot;name&quot;] = &quot;new name&quot;
    await put_config(config)


async def update_description():
    config = await get_config()
    config[&quot;description&quot;] = &quot;new description&quot;
    await put_config(config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段不同，听起来互不影响。可一旦两个工具读到同一个旧版本，后写入的那个就会把前一个的修改盖掉：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;T1: read  {name: old, description: old}
T2: read  {name: old, description: old}
T1: write {name: new, description: old}
T2: write {name: old, description: new}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是丢失更新，Lost Update。&lt;code&gt;gather&lt;/code&gt; 做了它该做的事，把两个协程并发跑起来；它并不知道这两次写入在业务上会发生冲突。&lt;/p&gt;
&lt;p&gt;所以，判断能不能并行时，光看函数是不是 &lt;code&gt;async&lt;/code&gt; 不够，还得看它们操作什么资源、怎样写入。对于共享配置，可以考虑串行执行、原子字段更新，或者在写入时检查读取到的版本。幂等键能处理重复请求，却不能单独解决两个不同请求互相覆盖的问题。&lt;/p&gt;
&lt;p&gt;我把可运行的小例子和几种修复方式放在了&lt;a href=&quot;/notes/parallel-tool-calls-lost-update&quot;&gt;这篇笔记&lt;/a&gt;。这里先记住一件事：模型提出的调用顺序，不能替代系统对共享状态的判断。&lt;/p&gt;
&lt;h2&gt;点了取消，事情就结束了吗&lt;/h2&gt;
&lt;p&gt;启动子 Agent 也很容易写成一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;task = asyncio.create_task(run_sub_agent(query))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但一个子任务跑起来以后，可能已经打开文件、创建临时目录，或者向远程服务发出了请求。用户取消任务时，系统要处理的是这些正在发生的事。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;task.cancel()&lt;/code&gt; 会发出取消请求，任务需要有机会响应。它不会让远程请求倒着执行，也不会自动收回已经写出去的数据。更麻烦的是，子任务有时会晚一点才返回：父任务已经结束，它却带着旧结果回来，准备继续写入。&lt;/p&gt;
&lt;p&gt;我会把这件事分成两部分处理。一部分是尽快让工作停下来：传播取消、等待清理、为清理设置时限。另一部分是在提交处挡住过期结果：检查任务是否还有效、它使用的授权或版本是否已经失效。&lt;/p&gt;
&lt;p&gt;资源也要分清楚。子任务自己创建的临时目录可以由它清理；从父任务借来的连接，不能在退出时顺手关掉。否则一个子任务取消了，旁边仍在运行的任务也会跟着出问题。&lt;/p&gt;
&lt;p&gt;这也是我觉得“能启动多少个 Agent”之外，另一个值得看的指标：任务不想继续了，系统能不能把它妥善收尾。具体的生命周期和资源处理，放在&lt;a href=&quot;/notes/sub-agent-cancellation-resource-ownership&quot;&gt;子任务取消笔记&lt;/a&gt;里。&lt;/p&gt;
&lt;h2&gt;参数正确，只是走完了第一步&lt;/h2&gt;
&lt;p&gt;再看一个文件工具请求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;action&quot;: &quot;file_process&quot;,
  &quot;abs_path&quot;: &quot;/workspace/project/report.md&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它可能完全符合 Schema，路径也确实存在。但这些只能说明请求格式没有问题。文件属于谁，当前任务能不能访问，目标是不是只读资源，都还没检查。&lt;/p&gt;
&lt;p&gt;路径本身也会带来麻烦。&lt;code&gt;..&lt;/code&gt; 可能把目标指向另一个目录，符号链接可能把访问带出工作区；检查结束到真正打开文件之间，目录还可能被替换。因此，执行器需要把授权绑定到实际操作的资源，而不是只看模型提供的字符串。&lt;/p&gt;
&lt;p&gt;读到的内容又是另一层问题。网页或文件里出现一句“忽略之前的规则”，它仍然只是外部数据。不能因为它被放进了上下文，就让它变成新的授权依据。&lt;/p&gt;
&lt;p&gt;我会让各层各自负责一件事：Schema 校验参数，权限代码检查主体和资源，工具执行器完成操作并记录结果。提示词可以帮助模型理解边界，但真正拦住越界操作，还得靠执行代码。&lt;a href=&quot;/notes/tool-schema-policy-mcp-file-authorization&quot;&gt;文件授权那篇笔记&lt;/a&gt;把这条流程拆得更细。&lt;/p&gt;
&lt;h2&gt;把检查放在事情真正发生之前&lt;/h2&gt;
&lt;p&gt;说到这里，似乎每走一步都要检查，会不会让 Agent 变得很笨重？&lt;/p&gt;
&lt;p&gt;我觉得关键在于检查放在哪里。生成几个候选方案、比较资料、重写一段草稿，这些地方可以给模型比较大的尝试空间。准备覆盖共享文件、发布内容，或者把数据发送出去时，系统就需要核对当前授权和状态。&lt;/p&gt;
&lt;p&gt;可以把提交前的检查理解成这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Candidate action
    ↓
Commit Boundary
    ├── 是否有权限？
    ├── 状态是否仍然有效？
    ├── 是否可以并行？
    ├── 是否需要确认？
    ├── 能否取消或回滚？
    └── 是否留下执行证据？
    ↓
External side effect
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的“需要确认”也不是每次都弹窗。已经明确授权、范围没有变化的操作，可以按规则执行；超出约定范围、后果又难以撤回时，才需要把决定交还给用户。具体规则要跟任务一起设计。&lt;/p&gt;
&lt;p&gt;工作流和自主规划也可以配合使用。确定的步骤交给代码，开放的问题留给模型，不必为了显得更“Agent”而把每个环节都变成一次推理。&lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot;&gt;Building Effective Agents&lt;/a&gt;里从简单方案开始的思路，我觉得很适合拿来提醒自己。&lt;/p&gt;
&lt;h2&gt;下次做功能时，我会先问什么&lt;/h2&gt;
&lt;p&gt;与其先决定要接多少工具，我更想先问：失败以后会留下什么？是一个可以重写的草稿，还是一份已经被覆盖的文件？任务能不能停下来，停下来之后有没有人清理？出了错，能不能找到当时的输入、授权和状态版本？&lt;/p&gt;
&lt;p&gt;这些问题的答案，会影响并行策略、权限范围和记录方式。成本、最大步骤数和时间限制也要在这里考虑，否则一次本来很小的探索，很容易变成没有明确终点的后台任务。&lt;/p&gt;
&lt;p&gt;我想保留的自由度，是模型可以在合适的范围里尝试不同路径。至于写入是否冲突、权限是否有效、资源有没有回收，最好不用靠它这一次恰好想起来。&lt;/p&gt;
&lt;p&gt;这也是“自主性预算”这个说法对我的意义：先看行动的后果，再决定让它自己做主到哪一步。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Sierra Research, &lt;a href=&quot;https://arxiv.org/abs/2406.12045&quot;&gt;τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apple, &lt;a href=&quot;https://arxiv.org/abs/2408.04682&quot;&gt;ToolSandbox: A Stateful, Conversational, Interactive Evaluation Benchmark for LLM Tool Use Capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ETH Zürich, &lt;a href=&quot;https://arxiv.org/abs/2406.13352&quot;&gt;AgentDojo: A Dynamic Environment to Evaluate Prompt Injection Attacks and Defenses for LLM Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Princeton et al., &lt;a href=&quot;https://arxiv.org/abs/2407.01502&quot;&gt;AI Agents That Matter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anthropic, &lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot;&gt;Building Effective Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a href=&quot;https://openai.com/index/unlocking-the-codex-harness/&quot;&gt;Unlocking the Codex harness: how we built the App Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yang et al., &lt;a href=&quot;https://arxiv.org/abs/2405.15793&quot;&gt;SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>