一次工具调用失效,有时不是工具坏了,而是模型计划没有被正确解析;一次源码总结不完整,也可能是系统把“读了第一页”误当成“读完”。可靠执行需要把输入格式和完成证据都处理清楚。
这是 Cognitive OS 技术系列第 5 篇,选三个实际工程问题说明模型输出如何进入执行链路。
这篇文章要解决什么问题?
本文围绕“从工具意图追踪到交付证据”展开。阅读顺序是:响应如何变为动作 → 动作如何校验执行 → 输入是否完整 → 故障怎样闭环 → 产物如何验收。先建立概念与执行顺序,再进入具体函数、异常分支和测试,避免把模块名当成理解系统的起点。
| 阅读阶段 | 要回答的问题 |
|---|---|
| 为什么模型输出工具意图还不够? | 先看实际故障:有调用文本或大量动作,并不能证明用户要求的产物已经出现。 |
| 外部协议如何变成内部动作? | 保留响应来源,识别协议边界,再处理结构完整性;不同格式不能套用同一修复规则。 |
| 动作执行前后需要哪些约束? | 解析出动作后还需要参数与权限校验,执行后则需要可信结果和可比较的变化。 |
| 文件分页和附件转换怎样保证输入有效? | 文件读取与附件转换都是输入链路:既要检查覆盖范围,也要检查模型实际收到的表示。 |
| 失败后怎样定位、修复与验收? | 先定位失败层,再给可操作的错误;最终验收仍回到用户目标,而不是最后一次工具返回。 |
| 怎样建立覆盖整条链路的回归? | 结构测试、工具测试与产物验收分别保护不同边界,组合后才能防止故障反复出现。 |
文中的源码行为以原文标注的 cca98b6 为阅读基线;涉及“应该”“可以”或后续演进的内容属于设计分析与验证建议。历史基准和故障记录不代表当前版本每次运行都会产生相同结果。
1. 为什么模型输出工具意图还不够?
先看实际故障:有调用文本或大量动作,并不能证明用户要求的产物已经出现。
贯穿案例:从“生成 PPT”倒推工具链验收点
用户要求的是可打开、内容符合要求的演示文稿。倒推这项交付,至少需要有效的输入材料、可执行的生成动作、实际文件和产物检查。任何一段断开,模型都可能继续生成看似合理的总结。
| 可见现象 | 优先检查的边界 | 不能据此直接得出的结论 |
|---|---|---|
| 回复里出现工具标签 | Provider 适配与 parser 输出 | 标签出现不代表动作已被执行 |
| 读取工具返回成功 | 分页覆盖范围与转换内容 | 成功读一页不代表完整理解全部附件 |
| 生成脚本已经落盘 | 是否实际运行,运行参数是什么 | 有脚本不代表有演示文稿 |
| 命令退出码为 0 | 输出路径、大小、格式及打开结果 | 进程成功不代表业务内容合格 |
| 文件能够打开 | 页数、内容覆盖、文字可读性 | 格式有效不代表满足用户目标 |
排障时应保存同一任务下的原始响应、内部动作、工具结果和产物路径。先找到最早断开的边界,再决定补解析、修参数、续读文件还是重新生成产物。不要仅根据动作次数选择修复方向:90 个动作可能大多消耗在读取和错误恢复,执行数量不是交付进度的可靠代理。
后文的 DSML、字节分页与附件处理看似不同,实际分别回答同一个问题的不同部分:执行意图有没有被接住,执行输入是否完整,最终结果是否可核对。表格提供通用排障顺序;已有故障的具体原因仍以正文保存的 trace 和复盘为依据。
DSML 出现在答案里,却没有工具执行
用户曾遇到这样的记录:模型输出了多段 DSML 工具调用,界面却显示“本次未调用工具”。模型表达了动作,Runtime 没有把该格式还原为合法计划,自然也就没有真正执行。
Planner 因此需要兼容项目覆盖的 JSON、标签和 DSML 形式,将工具名与参数转换为内部动作数组。重要的不是“能从字符串中找到一个工具名”,而是整批计划中的每个动作都能正确恢复。
一个危险的折中是:某一段解析失败,就丢弃它,却仍把整项任务宣布完成。项目对 DSML 采用整批校验:非法或不完整结构需要明确失败,不能静默遗漏动作。JSON 截断的恢复策略另有处理,后文会区分这两条路径。
兼容解析也有边界。支持若干协议变体,不等于可以执行任意模型文本。结构合法之后,仍需工具查找、参数处理、策略与实际执行。
一次 90 个动作的任务,为什么仍没有 PPT
这类问题在真实日志中出现过:用户要求整理技术项目的深度 PPT,任务运行 21 轮,记录 90 个工具动作,然后状态变成 DONE。拆开动作才发现,88 次是读取与检索,另外两次 shell 分别检查 python-pptx 是否可导入,以及列出现有产物文件。
记录里最后阶段出现了四个 file_read 的 DSML 调用,同时有一轮 planner 响应未能被接受并返回“没有执行动作”。不过后续仍然成功写入辅助修复脚本,因此现有证据不足以把整次失败归因于 DSML 解析器。真正阻止交付的主因仍是生成脚本的语法错误没有全部修完、生成命令没有重跑。目录里旧 PPT 的存在也容易造成错觉:有文件,不等于本次任务生成了文件。
进一步核对这次运行时,安装目录中的二进制与刚生成的安装包时间一致,不能把问题归因于旧程序。真正的失败点是生成脚本含有 4 处 Python 元组语法错误,Agent 只修正其中 1 处,后续又有两条 shell 执行失败;虽然写入了辅助修复脚本,却没有再次运行生成命令,也没有核对本次产生的 PPT。运行日志最终把“已写入脚本”当成进展,任务状态则被误判为完成。
这次事故同时暴露了自修复流程与完成证据两个缺口:工具失败后应该读取完整报错,修正所有同类问题,重跑原命令,并检查产物存在、可读取;另一方面,Runtime 需要区分“用户要求的产物已经写入”和“为了产出物而写了一个脚本”。修复加入了 Python/脚本/构建/测试失败后的错误分析与产物核对指引,并补了 planner 回归,确认失败信息和重新验证要求会进入下一轮规划。该修复改善了 agent 的后续指导,不等于已经具备通用的文件产物契约;独立校验器仍然更可靠。
这也是日志里应该带构建来源的原因。版本号可能未变化,源码分支也可能比安装包更新;定位时应记录可执行文件路径、构建时间或提交信息,并确认监听端口的实际进程。本文的案例只引用任务相关信息,没有必要公开用户模型配置或凭据。
2. 外部协议如何变成内部动作?
保留响应来源,识别协议边界,再处理结构完整性;不同格式不能套用同一修复规则。
不同 Provider 的兼容层应保留原始来源
OpenAI-style function call、纯 JSON 内容、标签式 DSML 可能描述相似动作,但调用 ID、arguments 字段与多模态载荷并不相同。adapter 应尽量保留 provider-native response、tool call ID 和 finish reason,归一化时再映射到内部动作对象。这样发生 parser 差异时可以回看原始结构,而不是只剩一份被压平的字符串。
兼容测试需使用不含真实密钥的脱敏响应集,按 provider/model family 归档格式变化。升级 SDK 或更改 adapter 后,跑旧响应回归;遇到新的 provider 格式先记录样本,再适配,而不是让宽松解析器猜字段。服务端返回“文本完成”与“tool calls finish”不同,终止原因会影响 planner 是否继续等待动作。
工具结果回传给模型也需要维护对应的调用关系。多次 tool calls 并行时,工具结果应关联原 call ID;若 adapter 将结果只按数组位置配对,乱序完成可能把输出传给错误的调用。当前是否支持并发工具计划,要查执行器实际行为,不能仅从 DSML 解析器支持多个 invoke 推导。
从外部格式到内部动作数组
Planner 最终需要得到类似下面的内部含义:
[
{"tool":"file_read","args":{"path":"source.c","offset_bytes":0}},
{"tool":"file_read","args":{"path":"notes.md","offset_bytes":4000}}
]
格式适配与工具执行应分开。适配器负责恢复工具名、参数对象和调用顺序;执行阶段负责查找工具、策略检查、运行、收集结果。即使格式解析正确,路径仍可能不存在,模型仍可能调用未知工具,网络仍可能失败。
DSML 支持不同竖线标记和参数形式;外层 calls/function_calls、invoke、parameter 的结构必须配对。args 或 arguments 作为对象封装时不能再混入冲突的散列参数。字符串中的类似标签内容也必须按负载处理,不能随意正则删除。
当前源码对顶层 DSML 响应采用完整校验,失败时明确返回错误。其他 JSON 格式存在截断修复与挽救前导完整动作的路径,不能把 DSML 的整批规则泛化成所有格式都保证原子执行;解析的一致性与执行事务的原子性也应分别讨论。
Parser 的职责是识别协议结构
可靠解析器应分开处理三个层次。第一层识别模型响应属于 JSON、标签式函数调用还是 DSML 结构;第二层完整解析调用名称、参数名和参数载荷;第三层使用对应类型的 JSON 解析器解码对象。协议边界负责确定“哪些字节是参数”,JSON parser 负责解释字符串转义、数组和嵌套对象。把两层混在一条正则里,极容易让正文内容影响执行。
对于多条 DSML 调用,解析器应保存顺序并统计数量,直到整个 calls 区块闭合。一个可读的状态机可以使用 OUTSIDE_CALLS、INSIDE_CALLS、INSIDE_INVOKE、INSIDE_PARAMETER、DONE 五种状态;每个 open 都应匹配对应 close,遇到未知嵌套或 EOF 未闭合时返回结构错误。字符串参数应被当作原文,不应对其中的标签字样做第二遍分割。
示意伪代码:
找到顶层 calls 起点
while 还有 token:
若遇 invoke 起点:要求当前不在另一个 invoke 内
若遇 parameter 起点:解析 name 和 string 属性
消费完整负载,直到协议识别的参数终止边界
若遇 invoke 终点:验证工具名、参数结构并追加动作
要求 invoke 与 calls 都闭合
验证动作数组不是空数组
真实协议解析还必须按照其转义与参数封装规则实现,伪代码没有定义所有合法语法。遇到字符串中包含字面闭合 token 的情况,必须依靠协议的长度或转义语义区分 payload 和控制 token;若格式本身无法无歧义表示,就应拒绝这个输入或规定编码方式,而不是猜测。
顶层位置也很关键。用户可能直接贴一段 <invoke>,问“这段调用表示什么”,这时它是待解释的数据,不是 Runtime 执行计划。规划器应优先识别 Provider 的结构化 tool call 字段或被允许的顶层编码;不应在任意最终答案、引用代码块和附件正文中搜寻工具标签。对于错误形态,返回给模型的 diagnostic 应指出哪个调用、哪个参数、哪一个闭合边界不合法,但需限制错误回显长度并保护敏感字段。
解析成功不意味着执行成功,也不意味着多个动作是事务原子执行。若第一个动作写文件成功、第二个 SSH 失败,前者是否回滚由事务策略和工具类型决定。数据解析的整批校验可以避免执行一个缺尾的半截计划,但它不能自动撤销开始执行后的外部副作用。应在文档和监控中区分 plan_parse、policy_check、tool_run、tool_commit 与 task_complete。
为什么不靠一个正则提取所有工具调用
工具参数可能包含嵌套对象、转义引号、代码正文,甚至含有看起来像闭合标签的文本。用“遇到第一个右花括号就结束”的匹配,会把合法内容截断。反过来,贪婪匹配又可能吞掉下一个调用。
更可靠的方式是识别协议边界,再让 JSON 解析器处理参数对象,明确区分字符串参数与结构参数。正则适合某些有明确边界的字段提取,不适合独自承担任意嵌套语言的解析。
另一个边界是示例文本。用户可能要求“解释这段 DSML”,附件可能只是文档里的工具示例。解析器只应处理符合预期位置的顶层计划,不能在任意正文中搜到一个 invoke 就执行它。源码测试也包含正文示例和 JSON 字符串内字面标签的情况。
截断 JSON 的修复策略不能套用到所有协议
长 JSON 计划可能在生成长度上限处被截断。工程上常见两种处理:向模型请求格式修复,或提取在截断点前已经闭合的完整动作并继续执行。后者可以减少浪费,但只有当动作边界明确、剩余文本无法改变已解析动作语义,且部分执行被产品允许时才安全。
DSML 的标签结构需要独立处理。调用参数里可能是多行文本,结束标签缺失时无法判断是计划截断还是正文里含有相似 token;因此完整 calls 区块失败时,应返回结构错误并要求模型修复,而不能只忽略最后一个动作后执行前面部分。即便 JSON 支持 salvage,也不能据此把 DSML 解析失败默默降级成部分成功。
失败测试至少需要覆盖:截断发生在工具名中、参数名中、字符串 payload 中、嵌套 JSON 对象中、动作闭合之后但 calls 未闭合时,以及完成多个合法动作后再跟一段垃圾文本。每个用例要明确预期是整批拒绝、前缀恢复还是请求模型修复。解析器不能只被“格式正常”样例检验。
还要限制资源消耗。极深 JSON 嵌套、超长字符串、大量空调用、重复字段和过多动作都可能消耗内存或 CPU。计划解析应设置总字节数、最大动作数、嵌套深度与单个参数大小上限,并在超过限制时返回可诊断错误。这些上限应与模型返回 token 上限和工具 schema 保持一致。
3. 动作执行前后需要哪些约束?
解析出动作后还需要参数与权限校验,执行后则需要可信结果和可比较的变化。
工具 schema 是执行边界的一部分
解析器能从响应里得到 file_read,但工具注册表还需要提供参数 schema、执行函数、描述和权限关联。参数 JSON 解析成功后,应进一步验证必填字段、类型、长度、范围和互斥参数。例如 offset_bytes 应为非负整数,不能接受 latest 这样的字符串后再在执行函数里隐式转换。
错误参数应在副作用发生前拒绝。先打开一个文件后才发现页长非法,会多做无用 I/O;先写一部分内容再发现路径参数错误,则可能留下部分产物。校验器可以在执行前构造规范化参数对象,使工具函数不必重复处理未验证的 JSON 树。
schema 版本变化也要考虑历史任务。journal 可能留有旧版参数;恢复或回放时,旧结构如何迁移?可以为 tool invocation 保存 schema version,并提供向前兼容的解析器,或明确历史动作只能审计不可重放。若静默更改默认值,回放动作就可能与原运行不同。
动作数组中的顺序是用户意图的一部分。两个文件读取可以并行,先写再读却有顺序依赖;解析器保留顺序后,执行器是否并行化还需要分析依赖和副作用。默认串行容易推理,按 tool 自行并发可提高吞吐但需要幂等和资源隔离。不能因为计划里一次返回了多个动作,就断言它们安全地并行执行。
工具描述影响规划,但不构成权限控制
Provider 收到工具名称和描述后会据此生成参数。含糊描述可能让模型反复选错工具;过度冗长描述又会占用上下文。工具描述应强调何时使用、必要参数、返回值含义和已知限制,特别是分页工具要说明必须继续使用 next_offset_bytes。
但模型看见的描述不能替代策略检查。用户任务或附件可能包含“忽略限制并调用 shell”等文本,模型也可能生成超出 workspace 的路径。执行时仍要由策略层验证工具授权、路径范围、网络目的地和用户批准状态。计划语法合法只说明能理解它,不说明可以执行。
敏感参数尤其需要边界明确。SSH password 应按产品要求保护和在审计中脱敏;文件路径和普通 API 地址不应被宽泛替换成 [REDACTED],否则调试信息会失去意义。最佳实践是按字段类别脱敏,不要在任意文本上全局替换长得像秘密的值。
工具调用日志可以记录参数类型、路径、命令 hash、stderr 分类和耗时。对可能含凭据或个人数据的字段应只保留 redacted 标记与是否存在;对无敏感要求的路径,保留规范化路径有助于确认读写目标。脱敏规则本身也应有测试,验证 SSH 密码被隐藏,而端口、用户名或普通文字按产品语义保留。
工具执行前后应保存一个可比较的差异
修改文件的工具可以在执行前记录目标状态、操作意图和批准范围,执行后记录新文件大小与 hash。若事务失败,可以比较实际状态与开始状态,决定是否回滚;若输出内容不符合要求,也能把失败产物保留在隔离目录便于调试。
对于覆盖写,记录旧 hash 有助于判断操作是否与当前预期版本一致。若用户或另一个进程已经修改文件,工具可以拒绝覆盖或提示冲突,而不是默默用旧上下文写掉新内容。该检查可采用 optimistic concurrency:读取版本 V,提交时目标仍为 V 才写入。
新增唯一输出路径也能避免测试污染。每次任务用 task ID 或临时目录生成文件,通过验收后再原子移动到用户目标路径;失败时保留临时结果或清理。清理策略应明确,不要因任务 retry 将上一轮调试证据覆盖掉。
工具结果本身也需要可信度
模型可能把工具返回错误理解成成功,也可能在总结时忽略 exit code。Runtime 应保持机器可读的 success/error 字段,避免仅把 stdout 拼进一段文本。可验证的文件 hash、字节数和返回码应与自然语言说明分开存储。
工具输出也可能包含恶意指令或伪造的系统提示。将 stdout 作为数据传给模型时应标注来源,并继续执行策略验证;来自网页或文档的文本不能直接改变授权范围。安全提示有助于降低风险,但工具权限仍需由确定性代码限制。
4. 文件分页和附件转换怎样保证输入有效?
文件读取与附件转换都是输入链路:既要检查覆盖范围,也要检查模型实际收到的表示。
文件读取成功,不代表完整阅读成功
大型源码通常分多页读取。假设第一段已经成功返回,模型随即写出“项目完整分析如下”,Runtime 如果只看最后一句,就会提前结束。
项目的文件阅读链路会提供或追踪 offset_bytes、next_offset_bytes 和 EOF 等信息。对明确要求完整阅读的任务,未到 EOF 的文件属于尚未完成的工作;目录枚举也不能替代源码阅读。
读取 source.c 的第一段
→ 发现还有 next_offset_bytes
→ 继续读取后续字节
→ 核对 EOF
→ 根据完整观察整理结果
偏移使用字节口径,后续页应沿工具返回的偏移继续,而不是自行根据显示行数推算。模型请求过大的页长时,系统还需要明确说明实际截断值,避免“请求读很多”和“实际只拿到一部分”之间出现静默差异。
相关 HTTP 回归会把关键证据放在文件末段,再确认模型实际拿到该证据。只检查工具返回过 success,无法覆盖这个问题。
连续文件读取是覆盖范围证明
分页接口的 offset 是字节而不是字符。对于 UTF-8,工具会尽量让页尾落在完整字符边界,并返回下一次要读取的真实 offset。消费者应该保存这个 offset 原样继续,不应重新计算 offset + requested_max_bytes。如果读取结果还返回文件截断、实际字节数与 EOF,日志中应区分请求页长和实际读到的长度。
完整覆盖可以抽象成一个连续前沿 F。第一次读取从 0 开始,返回区间 [0, n) 就将 F 推进到 n。后续只有从当前 F 开始的读取才能继续推进。若工具允许乱序或重叠读取,应维护区间集合并合并连续区段;仅记录“读过某路径”或“某次看到 EOF”都无法证明所有字节无缺口。
一个简化伪代码:
frontier[path] = 0
result = file_read(path, offset=frontier[path], max_bytes=limit)
assert result.actual_offset == frontier[path]
assert result.bytes_read > 0 or result.eof
frontier[path] += result.bytes_read
if result.eof:
mark_complete(path, frontier[path], result.file_size)
实际工具可能会提供更丰富的 metadata,这段只是说明连续性逻辑。它还需要处理空文件、文件读取期间变短、从中间 offset 开始、符号链接和路径规范化。路径标识是否规范化会影响不同拼写映射到同一文件时追踪是否一致。
EOF 只说明某次读取到达该次打开文件视图的末尾。若文件在多个分页请求之间发生变化,连续 offset 也可能拼接出不同版本的内容。更强的完整阅读证明可以在首次读取记录 inode/file ID、大小、修改时间或内容 hash,并在分页结束时复核;在网络文件系统或高精度对抗场景下,mtime 可能不足,最好持有稳定文件句柄或生成快照。当前分页证据不应被描述成文件系统事务快照。
请求 4000 字节实际只返回 3998 字节,可能是为了避免在 UTF-8 字符中间截断。这个例子说明页数可能比按名义页长估计多,也说明完整阅读状态应由真实结果推进。大文件处理还要考虑工具输出进入模型上下文的成本:可以只摘要中间页,但关键代码、接口和末段证据应保留可回查的行号或 byte range。
分页工具的控制权属于返回值
完整读取流程最好把 next_offset_bytes 当作继续令牌,而不是模型记忆里的算术。每次动作收到工具结果后,planner 根据实际返回值选择下一 offset。若模型忽略它、重复 offset 或跳到未读区域,Runtime 可以比对待续读路径的连续前沿并指出缺口。
对大文件,页长受上限控制。请求 100,000 字节但工具最多返回 4000 字节时,结果应同时包含实际读取字节数、是否截断和下一 offset;如果只返回文本,模型可能误以为整段请求已满足。响应字段应该避免 size 同时表示原文件大小与本页大小,可以使用不同字段名表达。
控制前沿有一些细节:同一路径用 ./src/a.c 与绝对路径表示时是否归一到同一 key?目录符号链接会不会让一份文件有多个路径?分页时文件被改写,工具是在每页重新打开还是持有句柄?如果当前实现没有快照标识,日志应将完整覆盖限定为“工具返回序列连续覆盖”,不要升级成“同一版本文件完整读取”。
当用户要求读取多个文件,完成证据应逐个记录 path、起始 offset、连续区间终点、EOF 和文件标识。只看到最后一个文件 EOF 不应清除其他待续读路径。任务取消或重试时要定义已完成文件是否仍可复用、修改后是否重新读取。可被模型长期记住的状态比在一个 prompt 中临时记录前沿更可靠,但也需要清理失效路径。
中文源码分页,为什么偏移经常不是 4000 的整倍数
工具使用 4–4000 字节页长。中文 UTF-8 字符通常占多个字节,页尾可能落在字符中间。实现会尽量回退到完整字符边界,再返回实际读取长度,因此下一页起点可能是 3998、7998 等值。
请求:offset_bytes=0,max_bytes=4000
实际:页尾回退到 3998 字节
返回:next_offset_bytes=3998
正确后续:offset_bytes=3998
错误后续:自行按请求页长推算为 4000
如果跳到 4000,就可能漏掉字节;如果反复从零读取,动作数会增加却没有扩大覆盖范围。超大 max_bytes 会被限制到实际页上限,后续仍必须使用返回值,不能按请求的大小推进。
追踪完整性还应关注连续覆盖。读了 [0,3998) 和 [7998,EOF),即使后者显示 EOF,中间仍有缺口。Runtime 为待续读路径记录连续前沿,目的就是避免用“见过 EOF”替代“连续读到 EOF”。
这项机制也有明确边界:它跟踪读取动作与分页结果,不是文件系统的一致性快照。若文件在阅读期间被其他进程修改,还需要版本或内容校验才能证明读到的是同一个文件版本。这是进一步提高完整性保证时应补的条件。
支持附件,需要区分三层能力
用户上传图片、Word、PPT、PDF 或视频时,至少存在三个不同阶段:接收与保存文件、提取可用表示、让模型理解这些表示。
项目在附件链路中记录类型及解析元数据;文档可提取文本,多模态内容可形成图片或视频采样帧等输入。视频帧理解属于采样观察,不能等同于完整逐帧理解,更不能据此宣称音轨转写、每个动作识别都已具备。
实际效果依赖格式、可选解析依赖和所用模型。扫描件可能需要 OCR,图片输入需要兼容视觉能力的模型接口,损坏文件或不支持的格式应该返回诊断信息。文件扩展名出现在列表里,不是解析质量的证明。
另一个边界是请求时间。图片解析或视觉模型迟迟不返回时,用户需要看到阶段、超时与错误,而不是无限等待。相关回归通过慢速本地 provider,验证超时后能结束,并能继续下一次正常请求。
附件是一个转换流水线,不是一个文件扩展名
附件进入模型之前会经过若干转换。接收层要验证大小、MIME 类型与扩展名的关系并将文件写到受控目录;提取层可能解析 PDF 文本、Word 段落、PPT 页面或视频帧;组织层选择提取结果并构造 Provider 输入;Provider 必须支持相应模态;完成层再核对用户希望得到的交付物。每一层都可能失败或只给出部分表示。
对格式处理,最好保留“识别结果”和“能力”矩阵:
| 类型 | 可提取表示 | 常见缺口 | 需要的验收证据 |
|---|---|---|---|
| 可搜索 PDF | 页面文本、页数 | 扫描页无 OCR、双栏顺序 | 指定页面含末段标记且页码正确 |
| DOCX | 段落和表格文本 | 样式、浮动图、修订和页眉 | 标题、表格单元与段落都可检索 |
| PPTX | 页面文字、备注或图像 | 图表语义、动画、隐藏页面 | 页序和指定对象完整 |
| 图片 | 原图或视觉输入 | 方向、尺寸、视觉模型能力 | 小型视觉问答与 Provider 记录 |
| 视频 | 采样帧或音轨处理结果 | 采样间隔、音频未转写、动态遗漏 | 时间戳采样表、音频处理状态 |
这张表是诊断框架,并非每个格式在项目的每种构建中都能无条件支持。解析依赖、运行环境和模型接口都会改变实际可用能力。用户上传成功只能证明文件被接收,扩展名出现在 UI 中也不能证明内容已解析或传入模型。
扫描 PDF 的文本为空时,系统应返回“需要 OCR 或当前提取器无法读取”,而不是提供一份看似完整的空总结。视频抽帧应报告采样策略和时间戳;如果按固定间隔抽 5 帧,不能推断一个 5 分钟事件完整连续地被观看。PPT 中把图表截图成图片与提取表格数据也会产生不同的可读信息,必须按任务要求选择表示。
超时也要分层。文件转换等待、外部 ffmpeg 或 OCR 子进程、视觉 Provider 请求可能是不同耗时点。每层都需要有单独超时和进程终止/清理策略,错误中提供阶段名、格式、模型类型和耗时,不泄露 API 密钥或完整隐私数据。超时后还应验证临时文件清理和下一份附件仍可正常处理。
附件处理的诊断树
面对“图片一直卡住”或“PDF 解析空白”,先定位层次,再换模型。可以按下面的路径排查:
| 层次 | 要观察的事实 | 常见误解 |
|---|---|---|
| 上传 | 保存路径、大小、类型识别 | 接收成功就代表解析成功 |
| 提取 | 提取文本、页数或帧元信息、警告 | 扩展名在列表里就全部可读 |
| 输入组织 | 文本预览、原文件路径、图片/帧数 | 所有原文都已发送给模型 |
| Provider 请求 | 实际模型、超时、图片能力 | 模型名字像视觉模型就一定支持 |
| 完成核对 | 任务要求对应的输出与证据 | 给出一段总结就完成全部处理 |
PPT 文本提取不等于读取动画、图表和演讲者表达;扫描 PDF 的空文本可能是缺 OCR;视频采样帧可能遗漏采样点之间的事件。文章应描述已经拿到的表示,再说明由它能支持什么判断。
视觉等待还需要独立时间边界。当前请求有视觉专用超时设置,失败信息应说明模型与等待配置;超时后要释放当前任务状态,让后续正常请求仍可运行。只在前端显示“正在思考”,无法解释系统到底在等待提取进程还是 Provider。
5. 失败后怎样定位、修复与验收?
先定位失败层,再给可操作的错误;最终验收仍回到用户目标,而不是最后一次工具返回。
工具失败后的自修复闭环
有错误输出还不够,Agent 要利用它完成一条闭环:
执行命令失败
→ 读取完整 stderr、退出码与相关上下文
→ 归类语法、路径、依赖、权限或运行时错误
→ 修改所有相同根因
→ 重跑原命令
→ 检查退出码和本次预期产物
→ 打开或解析产物,确认内容达到请求要求
举例来说,脚本 py_compile 成功只能证明语法层通过;脚本运行退出码为 0 只能证明进程认为自己成功;目标 PPT 文件存在可能只是旧版本残留。更强的检查应在独立输出路径生成文件,核对修改时间或 hash、页数、标题、演讲者备注,并让解析器重新打开。用户要求的是 PPT 内容时,结构检查仍需结合页面渲染。
如果模型看到错误后仅写一个 _fix.py,却没有再执行它,任务就还没有完成。若它对四处相同语法错误只修一处,也应通过下一次完整运行让剩余错误暴露。Runtime 的失败提示因此需要明确说“修正所有同类错误”,不能只要求“试着再做一次”。
恢复提示需要保护任务边界。遇到权限拒绝或用户明确不能执行的操作,不应盲目绕过安全检查;应该解释失败并选用允许的替代方案。重复 SSH 或支付类动作前,需要先确认外部结果是否已经提交,避免“网络超时后重试”造成重复副作用。自修复不是无限重复同一命令,它需要根据错误类别做有上限的方案调整。
回归可使用 fake tool 返回固定 SyntaxError,检查下一轮模型输入确实包含原始诊断、退出码、重跑和产物校验要求;再由 fixture 返回一次成功与一次仍失败,分别验证 Runtime 会继续工作和保留失败状态。这样的测试检查控制流引导,不会声称它证明任意脚本都能被模型自动修好。
实际错误消息该包含哪些信息
工具失败响应最好给模型一个结构化摘要:tool、error_code、retryable、message、安全范围内的 stdout/stderr、exit_code、timeout_ms 和阶段。文件不存在通常不应按同一参数无限重试;临时网络错误可以有限退避;参数 schema 错误需先修输入;权限拒绝需保留策略原因并寻求允许的替代路线。
原始错误输出可能包含用户名、绝对路径、访问令牌或文件内容。进入 provider prompt 之前应按字段做脱敏和长度限制,但保留足以修复问题的行号、模块名和错误类别。若输出过长,最好展示开头、结尾及中间截断提示,并保存可供开发者审查的本地诊断文件。
恢复提示应给定重试预算并记录尝试次数。例如同一条不变的失败命令连续重试三次,只是重复浪费时间。模型应在读取错误后修改参数或脚本、采用不同的方法、把问题拆小;权限错误不应通过管理员 shell 绕开。可将重试策略分为“请求格式修复”“工具级暂时重试”“用户输入缺失需询问”和“不可恢复失败”。
用一个问题定位当前失败层
故障分析可以先回答六个是非问题:模型有没有生成计划?解析器得到几个动作?策略是否允许?工具函数是否开始执行?工具结果是否成功返回?任务完成判定是否核对了请求的结果?
| 观测结果 | 可能层级 | 下一条证据 |
|---|---|---|
| 回复里有调用,动作数为 0 | 协议/位置识别 | 保存原始响应、Provider 结构字段与 parser 错误 |
| 动作数正确但未知工具 | 注册表/工具名映射 | 可用工具清单与解析结果 |
| 策略拒绝 | 权限规则 | 规则匹配原因,不要先绕过策略 |
| tool 起始无结果 | 阻塞或进程等待 | 工具阶段计时、超时和进程状态 |
| tool 成功却无文件 | 参数或目标位置 | 规范路径、写入字节与文件 hash |
| 文件存在但内容不对 | 部分/陈旧产物 | 独立输出目录、内容与本次任务关联 |
| 所有证据齐全却显示失败 | 完成判定/状态展示 | task journal 最后状态与 reasoning synthesis |
用这张表能避免把所有故障都概括成“模型不听话”。如果计划根本没有被解析,改附件 OCR 没有帮助;如果写入路径错了,重新安装二进制可能只会重复相同错误。工具结果里要保留 exit code、短错误和原始 stdout/stderr 的受控截断,同时在日志里记录剩余字节数与原始结果 hash,方便进一步调查。
终态不能只根据最后一个工具结果
一条任务会经历多个模型与工具阶段,最后一次 shell 成功可能只是查看目录,用户要求的 PDF 仍没有生成。相反,纯知识问答没有任何工具动作,也可能完全达成目标。终态必须参考初始任务的交付要求与执行证据,不能简化为“最后一步返回 0”或“至少成功调用过一次工具”。
Runtime 可以为用户请求提取显式验收项,在末尾逐项检查是否有证据,例如文件存在、来源完整、输出格式和测试命令结果。这个提取仍可能误解自然语言,因此更可靠的工作流允许任务开始时确认目标路径和成功条件,或由工具调用接口将目标写成结构化字段。
当 LLM 返回总结而验收证据缺失时,下一轮应收到未满足项的明确清单。比如“PPT 生成脚本已写入,但命令未运行;目标 PPT 不存在”。模型可以继续;达到重试上限仍缺少产物时,系统把状态留为未完成并保留诊断,而不是将语言完整度等同于任务完整度。
如果输入同时包含解释和操作,比如“告诉我安全边界,然后创建文件”,完成契约需要两部分都核对。用户只获得风险解释而没有文件,任务仍部分未完成;只生成文件却没有回答安全问题,也同样不完整。状态可以由多个验收结果组合,必要时允许部分完成显示。
6. 怎样建立覆盖整条链路的回归?
结构测试、工具测试与产物验收分别保护不同边界,组合后才能防止故障反复出现。
属性测试和模糊输入能补足手写样例
协议测试常从几个固定字符串开始,但 parser 的问题通常藏在不同结构组合:参数顺序变化、引号转义、中文、多次调用、可选字段、嵌套数组、空字符串、标签样式变化和截断位置。Property-based test 可以随机生成内部动作列表,序列化为协议文本,再解析回来比较动作数量、顺序和 JSON 值。
往返测试只能验证 parser 与 serializer 相互兼容,二者可能共享同一个错误。因此还要加入独立构造的 golden fixtures 和 provider 实际响应样例。模糊测试则可以随机截断、插入未知标签、重复闭合、扩展空白和改变大小写;目标是确保 parser 要么得到合法受限结果,要么稳定返回错误,不崩溃、不无限循环、不执行半截调用。
测试 harness 应记录 seed,以便偶发错误能复现。每次失败后,缩小成最短输入并加入固定 regression。对最大长度和深嵌套输入,设置运行时间与内存上限,避免模糊测试本身成为拒绝服务。
复现测试要从 parser 延伸到用户能见的产物
每个层级都有不同的稳定性:parser unit test 最快,能穷举协议边界;本地 runtime integration 覆盖工具注册、策略与调度;HTTP e2e 覆盖请求 schema、进度序列化和最终状态;端到端 UI 测试确认用户看到的模型事件、滚动和文件链接。把所有断言塞进一个依赖真实 Provider 的 UI 测试,结果通常昂贵且偶发。
对于完整阅读,可以构造一个只有末尾才出现唯一标记的 UTF-8 文件,大小跨越多个最大页长;测试记录每次实际 offset,确认没有重叠之外的缺口且最终到 EOF。把页边界安排在 3 字节和 4 字节字符中间,验证 UTF-8 回退;再插入并发修改,确认实现对版本变化采用预期策略。
对于附件,测试不止“HTTP 返回 200”,还应读取导出的中间表示,检查预期页面、表格、图片尺寸、采样帧时间戳或明确的 unsupported 错误。解析失败和 provider 不支持视觉模态是不同原因,应分别注入失败。超时测试使用可控阻塞 Provider,等待状态进入指定阶段后释放或结束,而不是用随机网络延迟猜测。
对于生成文件,先创建唯一空目录,再运行真实的脚本与 Runtime,核验本次产物路径、打开内容和页数。旧目录里的同名文件应被排除在通过条件之外。若只是 exists(),残留旧文件就可能造成假阳性;若只看退出码,脚本也可能打印成功但遗漏生成步骤。
以分层回归核对执行链路
协议回归先在解析层检查动作数、顺序和参数,再经过真实 Runtime 执行文件写入与读回,分别验证格式还原和执行连通性。
分页用例把唯一证据放在末尾,使提前结束能够被观察,同时核对连续偏移,避免模型直接跳读末段。只检查一次工具成功或最终回答中的标记,都不足以证明完整覆盖。
本地慢速 Provider 能控制响应延迟、错误和格式,稳定复现超时边界。真实远程接口的偶发响应适合兼容性观察,却难以作为每次都可重复的控制流测试。
源码阅读基线:协议解析、字节分页与 UTF-8 边界、本次真实回复回归。
怎样观察真实执行
我希望演示可以回答:模型发出了什么动作?Runtime 解析出了几个?实际工具是否调用?分页是否结束?失败是哪一层产生?
在构建 CLI 后,可从这些回归进入:
cd backend
python tests/test_dsml_e2e.py
python tests/test_source_read_e2e.py
python tests/test_vision_wait_e2e.py
这些用例使用本地模型 fixture 和真实 Runtime/API,验证协议与控制流;它们没有测量生产模型对所有文件的理解准确率。
延伸阅读与实践顺序
完成本文后,建议选取“动作执行前后需要哪些约束?”中的一个分支,用固定输入记录预期行为、实际结果和差异。先验证一个具体不变量,再扩大任务规模;若结果不同,优先核对源码版本、配置与输入证据。
上一篇:Memory 与 Context;下一篇:任务完成与故障恢复。如果你遇到“看起来执行了,实际上没做”的案例,带上计划、工具结果和任务记录,会比单独给一句最终答案更有排查价值。