feat(multimodal): 内置多模态感知插件 + process.go 原生支持 tool message 多模态块

【新插件 internal/plugins/multimodal】
- see_picture(path): 读取本地图片/URL,base64 注入 image_url block,
  模型在下一轮 LLM 请求的 tool message 里直接看到图(1024×1024 图约 8500 token)。
  自动识别 MIME,限 3MB 防爆 context。
- see_video(path, frames): ffmpeg 提取关键帧,多帧作为 image_url block 注入。
  默认 4 帧,最大 10 帧,每帧限 2MB。
- listen(path): 读取音频文件,转为 audio_url block 注入,支持 mp3/wav/ogg/m4a。
  限 5MB。

【内核多模态 tool message 支持】
- agent/api 新增 ToolOutput 类型(为后续 handler 直接返回 blocks 预留)
- SDK 公共层新增 ContentBlock/ImageURL/AudioURL(OpenAI 多模态格式)
- IOManager 新增 SetToolBlocks/ConsumeToolBlocks(interface{} 避免循环依赖)
- PluginSDK.SetToolBlocks(blocks) 插件工具调用后注入 blocks
- ioAdapter 桥接 IOInjector.SetToolBlocks
- process.go 工具执行后消费 pending blocks → 追加到 tool message 的 Blocks 字段
  → MarshalJSON 输出 content 数组格式 → LLM 看到图/音频

【验证】
multimodal_see_picture 注入 1024×1024 PNG 后 llmsproxy 统计:
  prompt_tokens=44407(含 ~8500 image token),模型正确描述了图片内容。
This commit is contained in:
JianFeeeee
2026-08-27 08:39:21 +08:00
parent f0cdbdb030
commit b777322b95
8 changed files with 368 additions and 1 deletions

View File

@ -98,6 +98,8 @@ type Agent struct {
// 模型思考模式thinking/reasoning
thinkingEnabled bool
pendingToolBlocks []interface{} // 插件工具通过 SetToolBlocks 注入的多模态块type agentAPI.ContentBlockprocess.go 消费后追加到 tool message
// 启动时间
startTime time.Time

View File

@ -12,6 +12,7 @@ import (
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response string, toolsUsed []string, toolResults []ToolResultItem, err error) {
@ -311,7 +312,31 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
contentOnce = false
}
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: msgContent, ReasoningContent: resp.ReasoningContent, ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
// 多模态工具结果:插件通过 SDK.SetToolBlocks 注入 image_url/audio_url block
// process.go 拾起并追加到 tool message 的 content 数组OpenAI 多模态格式),
// 让下一轮 LLM 请求在 tool message 里看到图/音频。
toolMsg := agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}
if rawBlocks := a.io.ConsumeToolBlocks(); len(rawBlocks) > 0 {
var blocks []agentAPI.ContentBlock
for _, b := range rawBlocks {
if cb, ok := b.(pubsdk.ContentBlock); ok {
// 跨包类型拷贝pubsdk.ContentBlock → agentAPI.ContentBlock
block := agentAPI.ContentBlock{Type: cb.Type, Text: cb.Text}
if cb.ImageURL != nil {
block.ImageURL = &agentAPI.ImageURL{URL: cb.ImageURL.URL, Detail: cb.ImageURL.Detail}
}
if cb.AudioURL != nil {
block.AudioURL = &agentAPI.AudioURL{URL: cb.AudioURL.URL}
}
blocks = append(blocks, block)
}
}
if len(blocks) > 0 {
toolMsg.Blocks = blocks
}
}
msgs = append(msgs, toolMsg)
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,