知识点思维导图
29 个知识节点
Harness Engineering(04) - 工具注册、路由与执行
读完后,你应能完成以下任务:
- 绘制“Harness Engineering(04) - 工具注册、路由与执行 / 工具调用的本质:模型只是"点菜",上菜的是 harness”的关键对象与数据流,解释“它做的只是输出一段结构化的"我想调用 read_file,参数是 path=a.txt"——相当于在菜单上点了道菜。”,并用源码位置、日志或 Trace 标注证据。
- 为“Harness Engineering(04) - 工具注册、路由与执行 / 写好工具 schema:description 是重灾区”设计正常与异常输入,验证“一句话说清"干嘛用" -> 举例"什么时候该用我"(最关键,直接提升选择准确率) -> 标注边界/限制(覆盖范围、副作用、不能干什么)”,输出首个偏差位置与回归测试结果。
- 实现“Harness Engineering(04) - 工具注册、路由与执行 / 工具一多,怎么让模型选得准”的最小代码或配置,检验“名字本身就是给模型的第一层提示。”,输出命令、结果与 Diff,并说明不适用边界。
第 03 章你的 Agent 已经会用 2 个工具了。但真实的 Agent 动辄几十个工具——这时新问题来了:模型怎么知道该用哪个?选错了怎么办?同时要用好几个怎么办?工具报错了会不会把整个 Agent 带崩? 这一章就解决"工具一多就乱"的问题。
一、工具调用的本质:模型只是"点菜",上菜的是 harness
先把一个最关键的认知钉死:
模型从头到尾没有执行过任何工具。 它做的只是输出一段结构化的"我想调用
read_file,参数是path=a.txt"——相当于在菜单上点了道菜。真正下厨、上菜的,是 harness。
这个分工带来一个直接结论:工具好不好用,一半取决于"菜单"写得好不好。 菜单(schema)写得含糊,模型就点错菜。所以这一章很大篇幅在讲怎么写好"菜单"。
二、写好工具 schema:description 是重灾区
一个工具的 schema 有三个关键部分,重要性排序是:description > 参数描述 > 参数类型。
新手最容易犯的错,是把 description 写成"我是什么",而不是"什么时候该用我":
为什么差别这么大?因为模型选工具时,眼里只有 description。它不会去读你的函数实现。description 就是工具的"自我介绍 + 使用说明",写得越具体,模型选得越准。
写 description 的口诀:
- 一句话说清"干嘛用"
- 举例"什么时候该用我"(最关键,直接提升选择准确率)
- 标注边界/限制(覆盖范围、副作用、不能干什么)
三、工具一多,怎么让模型选得准
当工具从 2 个涨到 20 个,光靠好 description 还不够,还有几招:
① 工具别太多、别功能重叠。 如果有 read_file 和 read_text_file 两个看起来差不多的工具,模型会犹豫、会选错。功能重叠的工具要合并。经验上,一个 Agent 的工具控制在十几个以内最好用。
② 工具命名要"见名知意"。 get_user_orders 远胜于 guo 或 fetch_data_v2。名字本身就是给模型的第一层提示。
③ 用参数而不是用新工具。 与其做 read_first_line / read_last_line 两个工具,不如做一个 read_file(path, mode),让 mode 区分。工具数量越少,模型的选择负担越小。
四、一圈调多个工具:并行 vs 串行
模型在一圈里可以同时请求多个工具。比如你问"对比 a.txt 和 b.txt",它可能一次性请求 read_file(a) 和 read_file(b)。
harness 要处理好这种情况:
能并行就并行。 上面这些工具之间互不依赖(读 a 不依赖读 b 的结果),可以并发执行,大幅提速:
⚠️ 但要注意:有依赖关系的操作不能并行。比如"先创建文件夹再往里写文件",模型通常会分成两圈来做(先建、看到成功、再写),harness 不用操心;但如果它在同一圈里又建又写,并行就会出错。好在模型一般能自己判断这种依赖。
五、工具报错:把错误当"观察结果",而不是让程序崩
这是 demo 到产品最重要的一道坎。工具执行会失败——文件不存在、网络超时、参数非法……
错误做法:让异常往上抛,整个 Agent 崩溃。 正确做法:捕获异常,把错误信息当成一种正常的"观察结果"喂回给模型,让它自己决定怎么办。
这样做的妙处:模型收到"⚠️ 文件不存在"后,往往会自己纠错——比如先调 list_files 看看正确的文件名,再重新读。Agent 的"自愈能力",很大程度就来自这种"把错误喂回去"的设计。
六、安全:别让模型的输出直接拼进命令
如果你有个 run_command(cmd) 工具,直接把模型给的字符串塞进 os.system()——这是重大安全隐患。模型可能(被诱导)生成 rm -rf / 这种东西。
防护原则:
- 能用结构化参数就别用裸字符串。 比如删文件用
delete_file(path)而不是run_command("rm " + path)。 - 真要执行命令,用参数数组而非字符串拼接:
subprocess.run(["ls", path])而不是subprocess.run("ls " + path, shell=True),避免命令注入。 - 危险工具加确认/白名单。 这部分第 08 章会专门深入。
七、常见误区
❌ 误区 1:工具越多越强。 恰恰相反,工具过多会让模型选择困难、容易选错。少而精好过多而杂。
❌ 误区 2:description 写给人看。 它是写给模型看的"选择依据"。别写营销文案,要写清"何时用、有何限制"。
❌ 误区 3:工具报错就让程序崩。 要把错误喂回模型,给它自愈的机会。
❌ 误区 4:把模型输出直接拼进 shell / SQL。 永远把模型输出当"不可信输入",用参数化方式处理。
八、最佳实践
✅ description 里务必写"什么时候用我"和"我的边界",这是提升工具选择准确率性价比最高的事。
✅ 工具数量克制,功能重叠的合并,用参数代替增加新工具。
✅ 统一用一个 safe_call 包装所有工具执行,把异常转成喂回模型的文本。
✅ 互不依赖的工具调用并行执行,提速明显。
✅ 危险操作走结构化参数 + 参数数组,杜绝注入。
九、动手实践:多工具 Agent:选择、并行与自愈
- 选得准:根据问题自动挑对工具(读文件 / 算数 / 查天气)
- 自愈:工具报错时,错误被当成"观察结果"喂回,Agent 能自己纠错
- schema 的威力:每个工具的 description 都写清了"什么时候用我"
三个工具:
| 工具 | 作用 |
|---|---|
read_file |
读文件内容 |
calculate |
计算数学表达式 |
get_weather |
查天气(mock 假数据) |
9.1 怎么跑
默认离线 mock,无需 API Key:
python agent.py
它会依次跑几个不同类型的问题,让你看到 Agent 每次都选了对的工具:
- "帮我算一下 (3 + 5) * 2" → 选
calculate - "北京今天天气怎么样" → 选
get_weather - "读一下 不存在.txt" → 选
read_file,报错后自动改去读存在的文件(自愈演示)
9.2 看点
- 工具选择:对照每个工具 schema 里的 description,理解模型为什么这么选。
- 自愈过程:留意"读不存在文件 → 报错 → Agent 改读别的文件"这一段,这就是"把错误喂回去"带来的自愈。
- 试着把某个工具的 description 改烂(比如只写"工具"),再看模型会不会选错——直观感受 description 的重要性。
十、总结
- 工具调用的本质:模型只是"点菜",上菜的是 harness:它做的只是输出一段结构化的"我想调用 read_file,参数是 path=a.txt"——相当于在菜单上点了道菜。
- 写好工具 schema:description 是重灾区:一句话说清"干嘛用" -> 举例"什么时候该用我"(最关键,直接提升选择准确率) -> 标注边界/限制(覆盖范围、副作用、不能干什么)
- 工具一多,怎么让模型选得准:名字本身就是给模型的第一层提示。
- 一圈调多个工具:并行 vs 串行:⚠️ 但要注意:有依赖关系的操作不能并行。
- 工具报错:把错误当"观察结果",而不是让程序崩:这是 demo 到产品最重要的一道坎。
- 安全:别让模型的输出直接拼进命令:如果你有个 run_command(cmd) 工具,直接把模型给的字符串塞进 os.system()——这是重大安全隐患。
10.1 可运行实验:编程工具调用与路径安全
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AC-05 在线实验</title>
<style>
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:#0f1211;color:#e7ece9;font-size:13px}.shell{padding:16px}.top{display:flex;justify-content:space-between;gap:16px;margin-bottom:14px}h1{margin:3px 0;font-size:18px}.id,.value{color:#68e0b5;font-family:ui-monospace,monospace}.summary{margin:4px 0;color:#a5afa9}.run{border:0;border-radius:6px;background:#68e0b5;color:#07110d;padding:8px 14px;font-weight:700}.grid{display:grid;grid-template-columns:minmax(220px,.8fr) minmax(0,1.8fr);gap:12px}.panel{border:1px solid #29322e;background:#141817;padding:12px}.control{display:grid;gap:5px;margin-bottom:11px}.head{display:flex;justify-content:space-between;gap:8px}select,input{width:100%;accent-color:#68e0b5;background:#0d100f;color:#e7ece9}.toggle{display:flex;justify-content:space-between;border-top:1px solid #29322e;padding-top:9px}.toggle input{width:18px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.metric{border:1px solid #29322e;padding:8px}.metric b{display:block;color:#68e0b5;font-size:16px}.stages{display:flex;gap:6px;overflow:auto;margin:10px 0}.stage{border:1px solid #8a6230;padding:7px;min-width:90px}.stage.ok{border-color:#367a61}.stage.fail{border-color:#8b4545}table{width:100%;border-collapse:collapse}td{border-top:1px solid #29322e;padding:7px}.diagnosis{margin-top:9px;border-left:3px solid #68e0b5;background:#101412;padding:9px;line-height:1.5}.danger{border-color:#ef7f7f}@media(max-width:680px){.top,.grid{display:grid;grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}}
</style>
</head>
<body>
<main class="shell">
<header class="top"><div><div class="id">AC-05 · DETERMINISTIC LAB</div><h1 id="title"></h1><p class="summary" id="summary"></p></div><button class="run" id="run">运行实验</button></header>
<section class="grid"><div class="panel"><div id="controls"></div><label class="toggle"><span>注入典型故障</span><input id="failure" type="checkbox"></label></div><div class="panel"><div class="metrics" id="metrics"></div><div class="stages" id="stages"></div><table><tbody id="rows"></tbody></table><div class="diagnosis" id="diagnosis"></div></div></section>
</main>
<script>
const scenario = { title: '编程工具调用与路径安全', summary: '在虚拟工作区中校验工具名、参数 Schema、路径与补丁验证。', controls: [
{ key: 'tool', label: '调用工具', type: 'select', value: 'apply_patch', options: [['read_file', 'read_file'], ['search_code', 'search_code'], ['apply_patch', 'apply_patch'], ['shell', 'shell']] },
{ key: 'path', label: '目标路径', type: 'select', value: 'safe', options: [['safe', 'src/formatter.ts'], ['outside', '../../secrets.env'], ['empty', '空路径']] },
{ key: 'postCheck', label: '写后检查', type: 'select', value: 'tests', options: [['none', '不检查'], ['diff', '检查 Diff'], ['tests', 'Diff + Tests']] }
] };
const controls = document.querySelector('#controls');
const failure = document.querySelector('#failure');
document.querySelector('#title').textContent = scenario.title;
document.querySelector('#summary').textContent = scenario.summary;
function renderControl(control) {
const label = document.createElement('label'); label.className = 'control';
const head = document.createElement('span'); head.className = 'head'; head.innerHTML = '<span>' + control.label + '</span><span class="value" data-value="' + control.key + '"></span>'; label.appendChild(head);
const input = document.createElement(control.type === 'select' ? 'select' : 'input'); input.dataset.key = control.key;
if (control.type === 'select') control.options.forEach(option => { const item = document.createElement('option'); item.value = option[0]; item.textContent = option[1]; item.selected = option[0] === control.value; input.appendChild(item); });
else { input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step || 1; input.value = control.value; }
input.addEventListener('input', updateValues); label.appendChild(input); return label;
}
function updateValues() { scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); document.querySelector('[data-value="' + control.key + '"]').textContent = control.type === 'select' ? input.options[input.selectedIndex].text : input.value + (control.suffix || ''); }); }
function readValues() { const values = {}; scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); values[control.key] = control.type === 'range' ? Number(input.value) : input.value; }); values.failure = failure.checked; return values; }
function stage(name, state, detail) { return { name, state, detail }; }
const aiStage = stage;
function clamp(value, minimum, maximum) { return Math.min(maximum, Math.max(minimum, value)); }
function simulate(values) { const fail = values.failure;
/** 工具名是否在可信白名单内。 */
const knownTool = ['read_file', 'search_code', 'apply_patch'].includes(values.tool);
/** 目标路径是否已解析且位于工作区。 */
const safePath = values.path === 'safe';
/** 写操作后是否有足够验证。 */
const verified = values.tool !== 'apply_patch' || values.postCheck === 'tests';
/** 工具调用是否满足全部执行前条件。 */
const accepted = knownTool && safePath && verified && !fail;
return { metrics: [[accepted ? 'EXECUTED' : 'BLOCKED', '调用状态'], [knownTool ? 'VALID' : 'UNKNOWN', 'Tool Schema'], [safePath ? 'INSIDE' : 'REJECTED', '路径边界'], [verified ? 'YES' : 'NO', '写后验证']], stages: [stage('工具发现', knownTool ? 'ok' : 'fail', values.tool), stage('参数校验', values.path === 'empty' ? 'fail' : 'ok', values.path), stage('路径解析', safePath ? 'ok' : 'fail', safePath ? 'workspace' : 'outside'), stage('执行', accepted ? 'ok' : 'fail', accepted ? 'virtual repo' : 'blocked'), stage('验证', verified ? 'ok' : 'warn', values.postCheck)], rows: [['虚拟目标', values.path === 'safe' ? '/workspace/src/formatter.ts' : values.path === 'outside' ? '/secrets.env' : '(empty)'], ['危险命令', values.tool === 'shell' ? '不在工具白名单,模型文本不会直接进入 Shell' : '未请求 Shell'], ['故障注入', fail ? 'Schema 字段缺失,执行前拒绝' : '参数结构完整']], diagnosis: accepted ? '工具调用通过白名单、Schema、路径和验证四层检查。' : '调用被安全层阻断,未发生真实文件写入。', danger: !accepted };
}
function render() { const result = simulate(readValues()); document.querySelector('#metrics').innerHTML = result.metrics.map(item => '<div class="metric"><b>' + item[0] + '</b><span>' + item[1] + '</span></div>').join(''); document.querySelector('#stages').innerHTML = result.stages.map(item => '<div class="stage ' + item.state + '"><b>' + item.name + '</b><div>' + item.detail + '</div></div>').join(''); document.querySelector('#rows').innerHTML = result.rows.map(item => '<tr><td>' + item[0] + '</td><td>' + item[1] + '</td></tr>').join(''); const diagnosis = document.querySelector('#diagnosis'); diagnosis.textContent = result.diagnosis; diagnosis.className = 'diagnosis' + (result.danger ? ' danger' : ''); }
scenario.controls.forEach(control => controls.appendChild(renderControl(control))); updateValues(); document.querySelector('#run').addEventListener('click', render); render();
</script>
</body>
</html>
学完自测
选择所有正确答案;提交后逐项核对判断依据。