feat: 昔涟工具扩展 — OpenAI Function Calling 集成 (网络搜索/网页抓取/IoT设备查询)

This commit is contained in:
2026-05-16 23:12:39 +08:00
parent 7f2961e63e
commit 1f5c2508d6
9 changed files with 1081 additions and 26 deletions
+29
View File
@@ -13,6 +13,19 @@ type Adapter struct {
provider LLMProvider
}
// OpenAITool 暴露给调用方使用的工具定义(与 openai.go 的 openAITool 等价)
type OpenAITool struct {
Type string `json:"type"`
Function OpenAIToolFunc `json:"function"`
}
// OpenAIToolFunc 工具函数定义
type OpenAIToolFunc struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// LLMProvider LLM提供商接口
type LLMProvider interface {
// Chat 同步对话
@@ -21,6 +34,12 @@ type LLMProvider interface {
// ChatStream 流式对话,返回一个channel逐token推送
ChatStream(ctx context.Context, messages []model.LLMMessage) (<-chan StreamChunk, error)
// ChatWithTools 同步对话(支持工具调用),tools 为 nil 时等价于 Chat
ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (*model.LLMResponse, error)
// ChatStreamWithTools 流式对话(支持工具调用),tools 为 nil 时等价于 ChatStream
ChatStreamWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (<-chan StreamChunk, error)
// ModelName 返回当前使用的模型名称
ModelName() string
}
@@ -43,11 +62,21 @@ func (a *Adapter) Chat(ctx context.Context, messages []model.LLMMessage) (*model
return a.provider.Chat(ctx, messages)
}
// ChatWithTools 同步对话(支持工具调用)
func (a *Adapter) ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (*model.LLMResponse, error) {
return a.provider.ChatWithTools(ctx, messages, tools)
}
// ChatStream 流式对话
func (a *Adapter) ChatStream(ctx context.Context, messages []model.LLMMessage) (<-chan StreamChunk, error) {
return a.provider.ChatStream(ctx, messages)
}
// ChatStreamWithTools 流式对话(支持工具调用)
func (a *Adapter) ChatStreamWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (<-chan StreamChunk, error) {
return a.provider.ChatStreamWithTools(ctx, messages, tools)
}
// ModelName 返回模型名称
func (a *Adapter) ModelName() string {
return a.provider.ModelName()
+102 -18
View File
@@ -55,11 +55,28 @@ type openAIRequest struct {
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
Tools []OpenAITool `json:"tools,omitempty"`
ToolChoice string `json:"tool_choice,omitempty"` // "auto", "none", or specific tool
}
type openAIMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Role string `json:"role"`
Content string `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
// openAIToolCall OpenAI工具调用
type openAIToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function openAIToolCallFunction `json:"function"`
}
type openAIToolCallFunction struct {
Name string `json:"name"`
Arguments string `json:"arguments"` // JSON string
}
// openAIResponse OpenAI响应结构
@@ -74,6 +91,7 @@ type openAIResponse struct {
type openAIChoice struct {
Index int `json:"index"`
Message openAIMessage `json:"message"`
Delta openAIMessage `json:"delta,omitempty"`
FinishReason string `json:"finish_reason"`
}
@@ -91,12 +109,17 @@ type openAIError struct {
// Chat 同步对话
func (p *OpenAIProvider) Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) {
resp, err := p.doChat(ctx, messages, p.config.Model, false)
return p.ChatWithTools(ctx, messages, nil)
}
// ChatWithTools 同步对话(支持工具调用)
func (p *OpenAIProvider) ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (*model.LLMResponse, error) {
resp, err := p.doChat(ctx, messages, p.config.Model, false, tools)
if err != nil {
// 尝试fallback模型
if p.config.FallbackModel != "" && p.config.FallbackModel != p.config.Model {
log.Printf("[LLM] 主模型 %s 调用失败,降级到 %s: %v", p.config.Model, p.config.FallbackModel, err)
return p.doChat(ctx, messages, p.config.FallbackModel, false)
return p.doChat(ctx, messages, p.config.FallbackModel, false, tools)
}
return nil, err
}
@@ -105,17 +128,22 @@ func (p *OpenAIProvider) Chat(ctx context.Context, messages []model.LLMMessage)
// ChatStream 流式对话
func (p *OpenAIProvider) ChatStream(ctx context.Context, messages []model.LLMMessage) (<-chan StreamChunk, error) {
return p.ChatStreamWithTools(ctx, messages, nil)
}
// ChatStreamWithTools 流式对话(支持工具调用)
func (p *OpenAIProvider) ChatStreamWithTools(ctx context.Context, messages []model.LLMMessage, tools []OpenAITool) (<-chan StreamChunk, error) {
ch := make(chan StreamChunk, 100)
go func() {
defer close(ch)
resp, err := p.doChatStream(ctx, messages, p.config.Model)
resp, err := p.doChatStream(ctx, messages, p.config.Model, tools)
if err != nil {
// Fallback
if p.config.FallbackModel != "" {
log.Printf("[LLM] 流式调用主模型失败,降级: %v", err)
resp, err = p.doChatStream(ctx, messages, p.config.FallbackModel)
resp, err = p.doChatStream(ctx, messages, p.config.FallbackModel, tools)
}
if err != nil {
ch <- StreamChunk{Error: err, Done: true}
@@ -193,14 +221,31 @@ type openAIStreamChoice struct {
}
// doChat 执行同步对话请求
func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage, modelName string, stream bool) (*model.LLMResponse, error) {
func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage, modelName string, stream bool, tools []OpenAITool) (*model.LLMResponse, error) {
// 转换消息格式
oaiMessages := make([]openAIMessage, len(messages))
for i, msg := range messages {
oaiMessages[i] = openAIMessage{
Role: string(msg.Role),
Content: msg.Content,
oaiMsg := openAIMessage{
Role: string(msg.Role),
Content: msg.Content,
Name: msg.Name,
ToolCallID: msg.ToolCallID,
}
// 转换工具调用
if len(msg.ToolCalls) > 0 {
oaiMsg.ToolCalls = make([]openAIToolCall, len(msg.ToolCalls))
for j, tc := range msg.ToolCalls {
oaiMsg.ToolCalls[j] = openAIToolCall{
ID: tc.ID,
Type: "function",
Function: openAIToolCallFunction{
Name: tc.Name,
Arguments: tc.Arguments,
},
}
}
}
oaiMessages[i] = oaiMsg
}
reqBody := openAIRequest{
@@ -208,6 +253,10 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
Messages: oaiMessages,
Temperature: 0.8,
Stream: stream,
Tools: tools,
}
if len(tools) > 0 {
reqBody.ToolChoice = "auto"
}
jsonBody, err := json.Marshal(reqBody)
@@ -251,25 +300,56 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
return nil, fmt.Errorf("API返回空choices")
}
return &model.LLMResponse{
Content: oaiResp.Choices[0].Message.Content,
FinishReason: oaiResp.Choices[0].FinishReason,
// 检查是否有工具调用
choice := oaiResp.Choices[0]
llmResp := &model.LLMResponse{
Content: choice.Message.Content,
FinishReason: choice.FinishReason,
Usage: model.Usage{
PromptTokens: oaiResp.Usage.PromptTokens,
CompletionTokens: oaiResp.Usage.CompletionTokens,
TotalTokens: oaiResp.Usage.TotalTokens,
},
}, nil
}
if len(choice.Message.ToolCalls) > 0 {
llmResp.ToolCalls = make([]model.ToolCall, 0, len(choice.Message.ToolCalls))
for _, tc := range choice.Message.ToolCalls {
llmResp.ToolCalls = append(llmResp.ToolCalls, model.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
})
}
}
return llmResp, nil
}
// doChatStream 执行流式对话请求(返回原始HTTP响应)
func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMMessage, modelName string) (*http.Response, error) {
func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMMessage, modelName string, tools []OpenAITool) (*http.Response, error) {
oaiMessages := make([]openAIMessage, len(messages))
for i, msg := range messages {
oaiMessages[i] = openAIMessage{
Role: string(msg.Role),
Content: msg.Content,
oaiMsg := openAIMessage{
Role: string(msg.Role),
Content: msg.Content,
Name: msg.Name,
ToolCallID: msg.ToolCallID,
}
if len(msg.ToolCalls) > 0 {
oaiMsg.ToolCalls = make([]openAIToolCall, len(msg.ToolCalls))
for j, tc := range msg.ToolCalls {
oaiMsg.ToolCalls[j] = openAIToolCall{
ID: tc.ID,
Type: "function",
Function: openAIToolCallFunction{
Name: tc.Name,
Arguments: tc.Arguments,
},
}
}
}
oaiMessages[i] = oaiMsg
}
reqBody := openAIRequest{
@@ -277,6 +357,10 @@ func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMM
Messages: oaiMessages,
Temperature: 0.8,
Stream: true,
Tools: tools,
}
if len(tools) > 0 {
reqBody.ToolChoice = "auto"
}
jsonBody, err := json.Marshal(reqBody)