调试、错误处理与成本优化
Workflow 跑起来不难,难的是跑出来的结果不对、或者跑得太贵。这篇讲实际踩坑中总结的调试方法和省钱技巧。
一、Agent 不按预期行事
这是最常见的问题:你让它列大纲,它写了篇全文;你让它返回 JSON,它给你加了段说明文字。
1. Prompt 不够具体
"帮我审查一下代码"和"逐行检查以下代码,每条问题输出:文件:行号 - 问题描述 - 严重程度(致命/一般/建议),没问题输出'通过'",效果天差地别。
改法:给格式、给枚举值、给示例。参考第 2 篇讲的 Prompt 写法。
2. 输出格式不对
让 Agent 返回结构化数据,它总爱加"好的,这是结果:"这种前缀。
改法 :用 schema 参数。传了 schema 之后,系统通过 constrained decoding 保证输出合法 JSON,返回的直接是 JS 对象,不用 JSON.parse:
php
const result = await agent('列出5个要点', {
schema: {
type: 'object',
properties: {
points: { type: 'array', items: { type: 'string' } }
},
required: ['points']
}
})
// result.points 直接用,不用解析
3. 该触发的 Custom Agent 没触发
主 Claude 靠 description 判断什么时候派子 Agent。description 写得太模糊,它不知道什么时候该用。
改法:description 里写清楚"什么时候用",用动词开头,包含触发场景关键词。
makefile
# 不好
description: 代码审查员
# 好
description: 审查代码变更,检查Bug、安全问题和代码风格。当用户要求review代码、检查提交、或分析代码质量时使用。
4. 上下文溢出
给 Agent 塞了太多内容,它处理不了或开始丢东西。
改法:任务拆小。不要把整个项目代码丢给一个 Agent,先让它定位文件,再让它读相关文件。搜索类任务只给相关片段,不给全文。
二、Workflow 常见错误
忘了 () =>
scss
// 错:agent 立即执行,parallel 拿到的是结果不是任务
await parallel([agent('A'), agent('B')])
// 对
await parallel([() => agent('A'), () => agent('B')])
症状:三个任务看起来"同时"跑了,但其实是串行的,或者报错。
忘了 await
scss
// 错:result 不是结果,是个 Promise
const result = agent('搜一下')
log(result) // [object Promise]
// 对
const result = await agent('搜一下')
meta 里写变量
arduino
// 错:meta 必须是纯字面量
export const meta = {
name: TOPIC + '-workflow', // 不行
phases: [getPhases()], // 不行
}
meta 在脚本执行前就被解析,不能有运行时的值。
phase 名字对不上
meta.phases 里的 title 必须和代码里 phase('xxx') 的字符串完全一致,否则进度面板分组不对。
Agent 返回 null 没处理
某个 Agent 失败了返回 null,直接拼字符串会出现 "null" 这几个字。
csharp
// 防空
const allMaterials = [chinese || '', english || '', github || ''].join('\n')
三、调试技巧
1. 用 phase 和 label 看清执行过程
csharp
phase('搜资料')
const result = await agent('搜中文资料', { label: '搜中文' })
进度面板会显示当前在哪个阶段、哪个 Agent 在跑、跑了多久。不写这些也能跑,但出问题时你不知道卡在哪。
2. 用 log 输出中间值
scss
const outline = await agent('列大纲')
log('大纲内容:' + outline.slice(0, 200)) // 只打前200字
log() 会显示在进度面板上。调试时把关键中间产物打出来看,比跑完看最终结果猜哪里错了高效。
3. 返回中间产物
不要只 return 最终结果,把中间产物也带上:
kotlin
return { draft, reviews, final }
跑完可以直接看到审查意见是什么、改了哪里。确认没问题后再精简返回值。
4. 先用小数据跑通
不要一上来就喂 10 个主题。先用 1 个主题跑通整个流程,确认每一步的输出符合预期,再加量。
5. 单独测试某个 Agent
Workflow 里某个 Agent 输出不对,把它的 Prompt 复制出来,在对话里单独跑,调 Prompt。比改完整脚本再跑一遍快。
四、错误处理
Agent 失败不影响其他
parallel 里一个 Agent 失败返回 null,其他的照常返回。这是特性不是 bug。但你要处理 null:
ini
const results = await parallel(tasks)
const valid = results.filter(Boolean)
给默认值
ini
const TOPIC = args.topic || '默认主题'
const materials = chinese || '(中文搜索无结果)'
关键步骤可以加重试逻辑
javascript
async function agentWithRetry(prompt, opts, retries = 2) {
for (let i = 0; i <= retries; i++) {
const result = await agent(prompt, opts)
if (result && result.length > 10) return result
log(`第${i + 1}次结果为空,重试...`)
}
return null
}
不过一般不需要,Agent 失败概率不高。关键步骤加一下就行。
五、成本优化
跑 Workflow 最容易忽视的就是 token 消耗。几个立竿见影的做法:
1. 模型分层:haiku 干粗活,sonnet/opus 干细活
| 任务 | 推荐模型 |
|---|---|
| 搜索、分类、找问题、格式化 | haiku |
| 写正文、综合分析、改稿 | sonnet(默认) |
| 复杂推理、架构决策 | opus(很少需要) |
dart
// 搜资料用 haiku
() => agent('搜中文资料', { model: 'haiku' })
// 写初稿用默认(sonnet)
const draft = await agent('写初稿')
在 agent() 选项里传 model 就行,哪个 Agent 该用便宜模型就给哪个加。phases 只管进度面板显示,不控制模型。
2. 能用 JS 处理的别派 Agent
javascript
// 浪费:让Agent去重
const deduped = await agent(`把以下列表去重:${list.join('\n')}`)
// 省钱:JS去重,瞬间完成,0 token
const deduped = [...new Set(list)]
去重、过滤、排序、统计、字符串拼接,这些全用 JS。Agent 只干需要"理解"的活。
3. 只给片段,不给全文
javascript
// 浪费:把整个搜索结果塞给Agent
const summary = await agent(`总结:${fullSearchResults}`)
// 省钱:先提取相关段落
const relevant = extractRelevant(fullSearchResults, TOPIC)
const summary = await agent(`总结:${relevant}`)
4. 审查类任务用 haiku
找 Bug、查格式、检查 AI 味,这些不需要强推理。haiku 便宜好几倍,效果差别不大。只有最终综合改稿时用好模型。
5. 别让 Agent 干它不需要干的事
scss
// 浪费:Agent搜完还写了篇总结
() => agent('搜资料并总结成一篇文章')
// 省钱:只搜,总结交给下一步
() => agent('搜资料,列出3个来源和摘要', { model: 'haiku' })
六、常见问题速查
| 现象 | 原因 | 解决 |
|---|---|---|
| Agent 没自动触发 | description 写得不好 | 写清触发场景和关键词 |
| 输出不是想要的格式 | Prompt 不够具体 | 用 schema 参数 |
| parallel 里的任务串行执行了 | 忘了 () => |
包成箭头函数 |
| 结果里出现 "null" | Agent 失败返回 null | 加 ` |
| 进度面板不显示阶段 | phase 名字和 meta 对不上 | 检查字符串完全一致 |
| 跑起来特别贵 | 都在用默认模型 | 搜索/分类换 haiku |
| Agent 输出被截断 | 内容太长 | 任务拆小,只给相关片段 |
| 新 Agent 文件不生效 | 需要重启会话 | 重启 Claude Code |
| 同样的输入结果不一样 | 温度/随机性 | 关键步骤加 schema 约束,或多跑几次取一致的 |
小结
dart
调试:phase + label 看进度,log 打中间值,return 带产物
排错:先查 () => 和 await,再查 meta 字面量和 phase 名
省成本:haiku 干粗活、JS 干杂活、只给片段不给全文、能并行不串行