From b15994d9642e956a7d1dc4545b31a00f4cbde738 Mon Sep 17 00:00:00 2001 From: JianFeeeee <109188060+JianFeeeee@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:22:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(seemedia):=20hear=5Faudio=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=89=BF=E8=AF=BA=E3=80=8C=E4=BD=A0=E4=BC=9A=E5=90=AC?= =?UTF-8?q?=E5=88=B0=E3=80=8D=EF=BC=8C=E5=A6=82=E5=AE=9E=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E4=B8=8D=E6=94=AF=E6=8C=81=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题(e2e 抓出,非猜测) 端到端跑 hear_audio 时模型答「老实说,这次我没法真的听到」,转而去数 WAV 字节。查日志: tool hear_audio result: {"note":"音频已投给你——请在下一步直接聆听..."} modal fallback: 0 block(s) transcribed, 0 skipped (provider=llmsproxy vision=true audio=false) 真因是**配置缺口 + 描述说谎**叠加: - 当前部署所有 LLM 源都是 audio=false(visionllm/llmsproxy 均如此) - core.input_processing.audio.fallback_provider 为空 ⇒ 无回退源 - 于是内核既不能直视、也无法转写,只发一条「拿不到音频」的 notice 而工具描述写死「你会直接听到它」、note 写「请在下一步直接聆听」—— 模型同时看到「你会听到」与「拿不到」,两句自相矛盾(这就是它转去数 WAV 字节的原因)。 ## 为何不改代码改行为 插件在调用时**无法**得知当前模型是否支持音频,SDK 未暴露能力查询接口 (只有 LLMAPI 的 Chat/ReloadFromConfig)。所以插件没有能力「先判断再决定 要不要注块」。正确做法是:内核继续做它该做的诚实降级(已在 modalfallback 里实现),插件这边**不许替内核承诺**。 ## 改了什么 - hear_audio 描述:从无条件承诺改为条件式(「若当前模型支持音频或配了回退源, 你会在下一步听到」),并明说:若都不支持,内核会明确告知拿不到(不会假装 听到),那时改用 transcribe_audio 或配置回退源 - handler 的 note:从「请直接聆听」改为「请确认是否真的收到;没收到就改用 transcribe_audio」 ## 判据(可杀变异,3/3 实测) - TestHearAudioDoesNotPromiseAudibility:描述不得含无条件承诺;必须指明 transcribe_audio 出路;必须如实说「不支持」 - TestHearAudioNoteIsHonest:note 不得说「直接聆听」;必须给替代方案 ★ 判据自身修了一个 bug(变异测试抓出):原先用「下一个 RegisterTool」做段 终点,但 hear_audio 是**最后一个**注册 ⇒ 一路截到文件尾,把 handler 的 note 也吞进来,于是 note 里残留的 transcribe_audio 让「删掉描述出路」的变异逃逸。 改用 "p.handleHearAudio)" 作终点后该变异被杀。 变异实测: F 描述改回无条件承诺 → FAIL ✓ G 描述删掉 transcribe_audio 出路 → FAIL ✓(修判据前曾逃逸) H note 改回「直接聆听」 → FAIL ✓ I 描述删掉「不支持」说明 → FAIL ✓ 18/18 通过;全量 homed 构建通过;二进制内已无旧假承诺、含新的诚实措辞。 Co-authored-by: JianFeeeee --- internal/plugins/seemedia/plugin.go | 22 +++++-- internal/plugins/seemedia/plugin_test.go | 84 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/internal/plugins/seemedia/plugin.go b/internal/plugins/seemedia/plugin.go index 41feb8c9..c52c19ea 100644 --- a/internal/plugins/seemedia/plugin.go +++ b/internal/plugins/seemedia/plugin.go @@ -166,9 +166,18 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { if err := s.RegisterTool("hear_audio", sdk.ToolDef{ Name: "hear_audio", - Description: "【亲自听音频】把一段音频**原样投给你自己**去听,你会直接听到它," + - "而不是读一段转写文字。需要判断语气/音乐/非语音声音时用它。" + - "(只想要文字转写、或你本身不支持音频时,用 transcribe_audio 更省。)" + + // ★ 不得无条件承诺「你会听到」(2026-10-11 e2e 抓出)。 + // + // 实测:当前部署里**所有** LLM 源都是 audio=false,且 + // core.input_processing.audio.fallback_provider 为空 ⇒ 内核既不能直视 + // 也无法回退转写,只会发一条 notice 说「拿不到音频」。此前描述写死 + // 「你会直接听到它」,与内核随后发的 notice 自相矛盾,模型据此困惑 + // (实测它转而去数 WAV 字节)。描述必须如实说明可能拿不到。 + Description: "【亲自听音频】把一段音频**原样投给你自己**去听。若当前模型支持音频" + + "(或配了可用的音频回退源),你会在下一步直接听到它。" + + "★若当前模型与回退源都不支持音频,内核会明确告诉你拿不到音频(不会假装听到)——" + + "那时请改用 transcribe_audio 拿文字转写,或请用户配置音频回退源" + + "(core.input_processing.audio.fallback_provider + 该源 audio=true)。" + "来源三选一:path / digest / url。", Parameters: srcObject(srcParam), ParallelSafe: false, @@ -239,9 +248,14 @@ func (p *Plugin) handleHearAudio(args map[string]interface{}) (interface{}, erro Type: "audio_url", AudioURL: &sdk.AudioURL{URL: url}, }}) + // ★ note 同样不得承诺「你会听到」:插件在调用时无法得知当前模型/回退源 + // 是否支持音频(SDK 未暴露能力查询),支持与否由内核在下一步判定。 + // 这里只如实说「已投递」,并指明没收到时怎么办。 return map[string]interface{}{ "status": "ok", - "note": "音频已投给你——请在下一步直接聆听(这不是转写文字)。", + "note": "音频已投递——请在下一步确认是否真的收到音频内容。" + + "若收到「不支持音频」之类的说明,说明当前模型与回退源都听不了," + + "请改用 transcribe_audio 拿文字转写。", }, nil } diff --git a/internal/plugins/seemedia/plugin_test.go b/internal/plugins/seemedia/plugin_test.go index 6cfe157e..0902b090 100644 --- a/internal/plugins/seemedia/plugin_test.go +++ b/internal/plugins/seemedia/plugin_test.go @@ -169,3 +169,87 @@ func TestSeeToolsAreNotParallelSafe(t *testing.T) { "SetToolBlocks 写的是当前 call 的块,并发会串台") } } + +// ---- ⑤ 不得承诺「你会听到」 ---- +// +// 2026-10-11 e2e 抓出:hear_audio 的注册描述写死「你会直接听到它」,而当前 +// 部署里所有 LLM 源都是 audio=false、且音频回退源为空 ⇒ 内核既不能直视也无法 +// 回退,只发一条「拿不到音频」的 notice。模型看到「你会听到」+「拿不到」两句 +// 自相矛盾的话,转而去数 WAV 字节(实测)。 +// +// 插件在调用时**无法**得知当前模型是否支持音频(SDK 未暴露能力查询), +// 所以描述必须如实说「可能拿不到」,并把出路(transcribe_audio / 配置回退源) +// 写清楚。这条判据跨实现有效:谁把承诺改回去,谁就重建了那个自相矛盾。 +func TestHearAudioDoesNotPromiseAudibility(t *testing.T) { + src, err := os.ReadFile("plugin.go") + if err != nil { + t.Fatal(err) + } + s := string(src) + + // 钉住 RegisterTool("hear_audio" 这一段(到该注册调用结束为止)。 + // ★ 不能用「下一个 RegisterTool」做终点:hear_audio 是**最后一个**注册, + // 那样会一直截到文件末尾,把 handler 的 note 也吞进来 —— 于是 + // note 里残留的 transcribe_audio 会让本判据误判为「描述合格」。 + // (这个 bug 是变异测试抓出来的:改掉描述里的出路却仍 PASS。) + i := strings.Index(s, `RegisterTool("hear_audio"`) + if i < 0 { + t.Fatal("找不到 hear_audio 注册") + } + seg := s[i:] + // 终点:该 RegisterTool 调用对应的 "}, p.handleHearAudio)" 之后。 + en := strings.Index(seg, "p.handleHearAudio)") + if en < 0 { + t.Fatal("找不到 hear_audio 注册调用的结尾") + } + seg = seg[:en] + // ★ 剔除注释行:本文件的注释里会**引用旧文案**说明为何不得这么写, + // 那不是承诺。判据只应检查真正会送到模型面前的字符串(Description/note)。 + var code []string + for _, line := range strings.Split(seg, "\n") { + t2 := strings.TrimSpace(line) + if strings.HasPrefix(t2, "//") { + continue + } + code = append(code, line) + } + seg = strings.Join(code, "\n") + + // ① 不得出现无条件承诺 + for _, bad := range []string{"你会直接听到它", "你会直接听到"} { + if strings.Contains(seg, bad) { + t.Errorf("hear_audio 描述不得无条件承诺可听性(当前配置下会与内核 notice 矛盾): 含 %q", bad) + } + } + // ② 必须说明「拿不到时怎么办」——即指向 transcribe_audio + if !strings.Contains(seg, "transcribe_audio") { + t.Error("hear_audio 描述必须指明拿不到音频时的出路(transcribe_audio)") + } + // ③ 必须点明可能不支持(诚实降级),不能只说好话 + if !strings.Contains(seg, "不支持") { + t.Error("hear_audio 描述必须如实说明「当前模型/回退源可能不支持音频」") + } +} + +// TestHearAudioNoteIsHonest 钉住 handler 返回的 note 同样不做虚假承诺。 +func TestHearAudioNoteIsHonest(t *testing.T) { + src, err := os.ReadFile("plugin.go") + if err != nil { + t.Fatal(err) + } + s := string(src) + i := strings.Index(s, "func (p *Plugin) handleHearAudio(") + if i < 0 { + t.Fatal("找不到 handleHearAudio") + } + seg := s[i:] + if j := strings.Index(seg, "\nfunc "); j > 0 { + seg = seg[:j] + } + if strings.Contains(seg, "请在下一步直接聆听") { + t.Error("handleHearAudio 的 note 不得说「直接聆听」——插件无法保证当前模型支持音频") + } + if !strings.Contains(seg, "transcribe_audio") { + t.Error("handleHearAudio 的 note 必须指明听不到时的替代(transcribe_audio)") + } +}