mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 21:03:16 +00:00
## 缺陷
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]
67 lines
2.5 KiB
Go
67 lines
2.5 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|