Files
HomeAgent/internal/lua/deepseek_stream_test.go
JianFeeeee 0fdb13749a fix(lua): deepseek 适配器的流式路径处理 tool_calls(此前全部丢失)
## 缺陷

deepseek.lua 的 tool_calls 处理只存在于 `transform_response`(**非流式**路径),
而 `transform_stream_chunk` 只透 content/done:

    return json.encode({ content = delta.content or "", done = (fr ~= nil) })

于是 deepseek 源在**流式**模式下工具调用全部丢失 —— 模型调不动任何工具,
且**没有任何报错**,只是"工具好像不听话"。

## 为什么难发现

- 非流式路径是好的 ⇒ 端到端手工测试也过
- 内核的 tool call 循环默认走**流式**(provider.go 的 stream 分支)⇒ 实际不可用
- 功能判据(core 包的批内测试)直接构造 `agentAPI.StreamChunk{}`,
  **绕过适配器** ⇒ 测不到这一层

配置里 `deepseek` 源预设指向 `adapters/deepseek.lua`,所以任何按预设配置
的用户都会踩到(生产当前未启用该源,配置里 deepseek 相关键为 0)。

## 修法

照 openai.lua 的做法在流式路径补上:OpenAI 兼容格式
`{function:{name,arguments}, id, type, index}` → homed 扁平结构
`{id, type, name, raw_arguments, stream_index}`,含 reasoning_content 透传。

两个容易踩的点也写进注释:
- **不能按 name 过滤**:流式续传片 name 为空但携带 arguments,
  内核按 stream_index 分桶累积
- **必须透传 stream_index**:否则多个分片并到槽 0、argsRaw 混拼

## 判据

新增 TestDeepSeekAdapterHandlesStreamToolCalls:喂两个含 tool_call 的分片,
断言 tool_calls 未被丢弃且 stream_index 正确。修前两条分片全被丢弃。

## 体检分类随之变化

    修前: ✓ [openai]                                       ✗ [deepseek ...]
    修后: ✓ [deepseek openai]                              ✗ [anthropic gemini github groq mistral ollama]
2026-09-27 18:31:42 +08:00

67 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lua
import (
"encoding/json"
"testing"
)
// TestDeepSeekAdapterHandlesStreamToolCalls 钉住「deepseek 源的流式模式也能工具调用」。
//
// ★ 为什么单独给 deepseek 写判据:
//
// 它的 transform_stream_chunk 只透 content/done,**完全不处理 tool_calls**
// (那部分代码只存在于 transform_response,即非流式路径)。于是:
//
// · 非流式请求 ⇒ 工具调用正常
// · 流式请求 ⇒ 工具调用**全部丢失**,模型只收到纯文本
//
// 而内核的 tool call 循环默认走流式(provider.go 的 stream 分支)。所以
// 配了 deepseek 源的用户,模型调不动任何工具,且**没有任何报错** ——
// 只是"工具好像不听话"。
//
// 这类缺陷极难察觉:功能判据(core 包的批内测试)直接构造 Go 结构体,
// 完全绕过适配器;而非流式的端到端路径又是好的。
func TestDeepSeekAdapterHandlesStreamToolCalls(t *testing.T) {
vm := NewVM(t.TempDir())
loadBundled(t, vm, "deepseek")
// 一个含 2 个 tool_call 的 chunk(首片 + 续传片各一)
frags := []string{
`{"id":"c","choices":[{"index":0,"delta":{"tool_calls":[` +
`{"index":0,"id":"t0","type":"function","function":{"name":"cmd_run","arguments":"{}"}}]}}]}`,
`{"id":"c","choices":[{"index":0,"delta":{"tool_calls":[` +
`{"index":1,"id":"t1","type":"function","function":{"name":"cmd_run","arguments":"{}"}}]}}]}`,
}
for i, f := range frags {
out, err := vm.CallTransformStreamChunk("deepseek", f)
if err != nil {
t.Fatalf("第 %d 片: %v", i, err)
}
var u struct {
ToolCalls []struct {
StreamIndex int `json:"stream_index"`
ID string `json:"id"`
Name string `json:"name"`
RawArguments string `json:"raw_arguments"`
} `json:"tool_calls"`
}
if json.Unmarshal([]byte(out), &u) != nil {
t.Fatalf("第 %d 片输出非法: %s", i, out)
}
if len(u.ToolCalls) == 0 {
t.Errorf("第 %d 片:deepseek 适配器的流式路径**丢掉了 tool_call**\n"+
" 输出:%s\n"+
" ⇒ deepseek 源在流式模式下无法调用任何工具,且无任何报错。\n"+
" 它的 tool_calls 处理只存在于 transform_response(非流式路径)。",
i, out)
continue
}
if u.ToolCalls[0].StreamIndex != i {
t.Errorf("第 %d 片的 stream_index = %d,应为 %d", i, u.ToolCalls[0].StreamIndex, i)
}
if u.ToolCalls[0].Name != "cmd_run" {
t.Errorf("第 %d 片 name = %q,应为 cmd_run", i, u.ToolCalls[0].Name)
}
}
}