第 3 卷
为什么叫“单调”?因为守卫越挂越多,结果只会越严、不会变松,一旦有人拒绝,加再多弃权也翻不了案。因为守卫是安全约束:如果守卫能“放行”,多道守卫谁说了算就得定条规则(比
如“后挂的说了算”),宽松守卫就能盖掉严格守卫,强度取决于挂载顺序,太脆弱。单调性把这个漏洞钉死:最保守的守卫说了算,与顺序无关。
我们跑一下 npm run start:ch06,看下面两段输出:
复制代码 1--- 挂一个守卫,拒绝问候 "World" ---
2{ isError: true, error: { message: '不允许问候 "World"' }, content: [ { type: 'text 3
4--- 再挂一个"想放行"的守卫(验证单调性)--- 5{ isError: true, error: { message: '不允许问候 "World"' }, content: [ { type: 'text
后一段额外挂了一个对所有调用都返回 undefined的守卫,结果照样被拒。这就是单调性:
弃权不等于允许。
再往前不必停:③ 只会换“停”信号(上面说过);④⑤ 上一讲讲过,我们直接跳到 ⑥ tools/post-execute。
post-execute:能拦也能改
结果一旦产生,先流到这里,不动工具一行代码就能改写结果的最后一关。三条路(官方类型 PostToolDecision):
复制代码 1type PostToolDecision =
| 2 | | {kind:'accept';content?:ContentBlock[];value?:never} | *//改呈现* |
|---|---|---|
| 3 | | {kind:'accept';value:JsonValue;content?:never} | *//改值* |
| 4 | | {kind:'block';feedback:ContentBlock[] } | *//拦下* |
三条路各自动谁:
复制代码 1⑥ post-execute 三选一:
2
| 3 | 改呈现 | accept {content} | 换content;value不动 |
|---|---|---|---|
| 4 | 改值 | accept { value } | 换value;content由render重算 |
| 5 | 拦下 | block | 两个都丢:改判isError,只留feedback |
上一讲讲的是工具定义时怎么产出 value 和 content;到了这一站,是你在运行时动手改它们。
改呈现:只换模型看到的文本(← 后是我加的标注):
复制代码
| 1 | --- post-execute放行但改写呈现(注意:只改呈现,值没动)--- |
|---|---|
| 2 | { |
| 3 | isError: false, |
| 4 | content: [ { type:'text', text:'[长输出]'} ], ←给模型看的换了:模拟长输 |
| 5 | value:'Hello, World!' ←给程序用的没动 |
| 6 | } |
改值:连 value 一起换,render 会把呈现重算一遍。这也是两种 accept 互斥的原因:
复制代码
| 1 | --- post-execute改写值(呈现会被render重算)--- |
|---|---|
| 2 | { |
| 3 | isError: false, |
| 4 | content: [ { type:'text', text:'新的长输出'} ], ←呈现也跟着重算,即给模型看的也变 |
| 5 | value:'REPLACED' ←给程序用的换了 |
| 6 | } |
复制代码 1--- post-execute 把成功结果改判为失败 ---
2{ isError: true, error: { message: '结果含受限称呼 "World",已拦截' }, ... }
出了 ⑥ 还有两关:⑦ finalizeContent 是工具自己的可选收尾,也能最后改一次 content,但没有 ⑥ 的拦下与改 value 之权;⑧ tools/result 只观察。至此,八关全
部走完。
Code Mode:调用工具的另一种方式
上一节的流水线,跑在“模型逐个点名工具”的循环里:每调用一次,一份结果文本进上下文,等模型决定下一步——工具一多,逐次往返就很贵。
Code Mode 换的不是流水线,是模型用工具的方式:native 给一堆工具名、逐个点名;
code 只给一个 run_code,工具写进程序里调,跑完只带回最终结果。
这个思路业界常称 programmatic tool calling(PTC);dsh 同名的 PTC 预设就是把它打包好,选它就能得到 Code Mode。这里我们直接讲最底层配置——mode:native(默认)、
code、both(两种并存),不写即 native。
复制代码 1await root.plugin(ToolRuntime, { mode: 'code' })*// 工具入口切成 code:模型从此只见到
本节换两个新工具:add 两数相加、env_probe 报运行环境。
模型视角立刻变窄:
复制代码