This is an automated email from the ASF dual-hosted git repository. alexstocks pushed a commit to branch book-fight in repository https://gitbox.apache.org/repos/asf/dubbo-go-samples.git
commit b24d48156a9a118f429ce3dcfe8fb7424053d8ca Author: alexstocks <[email protected]> AuthorDate: Thu Sep 4 18:17:10 2025 +0800 update book-flight --- book-flight-ai-agent/.env.example | 2 + book-flight-ai-agent/go-client/frontend/main.go | 7 + .../go-client/frontend/static/script.js | 139 ++++++++++++++--- .../go-client/frontend/static/style.css | 3 +- book-flight-ai-agent/go-server/agents/cot_agent.go | 30 +++- book-flight-ai-agent/go-server/cmd/server.go | 18 ++- book-flight-ai-agent/go-server/conf/config.go | 2 +- .../go-server/model/bailian/bailian.go | 169 +++++++++++++++++++++ .../go-server/model/bailian/options.go | 23 +++ .../go-server/model/ollama/ollama.go | 28 +++- 10 files changed, 380 insertions(+), 41 deletions(-) diff --git a/book-flight-ai-agent/.env.example b/book-flight-ai-agent/.env.example index ded3ae3a..9c4b902f 100644 --- a/book-flight-ai-agent/.env.example +++ b/book-flight-ai-agent/.env.example @@ -23,6 +23,8 @@ LLM_API_KEY = "sk-..." # Client Settings CLIENT_HOST = "tri://127.0.0.1" CLIENT_PORT = 20000 +# Web/Task 超时配置(单位:秒),默认 120 秒(2分钟),可根据需求调整 +TIMEOUT_SECONDS = 120 # Web Settings WEB_PORT = 8080 diff --git a/book-flight-ai-agent/go-client/frontend/main.go b/book-flight-ai-agent/go-client/frontend/main.go index 5e3fd5b7..788dc1c7 100644 --- a/book-flight-ai-agent/go-client/frontend/main.go +++ b/book-flight-ai-agent/go-client/frontend/main.go @@ -80,6 +80,13 @@ func main() { "OllamaModel": cfgEnv.Model, }) }) + // 新增配置接口:供前端获取超时等配置 + r.GET("/api/config", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "TIMEOUT_SECONDS": cfgEnv.TimeOut, // 传递后端解析的超时时间 + "WEB_PORT": cfgEnv.PortWeb, + }) + }) r.POST("/api/chat", h.Chat) r.POST("/api/context/new", h.NewContext) r.GET("/api/context/list", h.ListContexts) diff --git a/book-flight-ai-agent/go-client/frontend/static/script.js b/book-flight-ai-agent/go-client/frontend/static/script.js index 678c26ff..f10786bc 100644 --- a/book-flight-ai-agent/go-client/frontend/static/script.js +++ b/book-flight-ai-agent/go-client/frontend/static/script.js @@ -11,6 +11,35 @@ const inputInitHeight = chatInput.scrollHeight; let fileBlobArr = []; let fileArr = []; +// 1. 页面初始化时请求后端配置(新增) +async function loadConfig() { + try { + const res = await fetch("/api/config"); // 后端新增接口返回配置 + window.CONFIG = await res.json(); + // 统一超时字段名:使用与后端一致的 TIMEOUT_SECONDS(原 TIME_OUT_SECOND 废弃) + window.CONFIG.TIMEOUT_MS = window.CONFIG.TIMEOUT_SECONDS * 1000; // 转为毫秒(方便定时器使用) + } catch (err) { + console.error("加载配置失败,使用默认超时(2分钟)", err); + window.CONFIG = window.CONFIG || {}; + window.CONFIG.TIMEOUT_SECONDS = 120; + window.CONFIG.TIMEOUT_MS = 120 * 1000; + } +} + +// 2. 页面加载完成后执行配置加载 +window.onload = async () => { + // 确保window.CONFIG已经存在,如果不存在则初始化 + if (!window.CONFIG) { + await loadConfig(); + } else { + // 确保window.CONFIG有TIMEOUT_MS属性 + if (!window.CONFIG.TIMEOUT_MS && window.CONFIG.TIME_OUT_SECOND) { + window.CONFIG.TIMEOUT_MS = window.CONFIG.TIME_OUT_SECOND; + } + } + // 其他初始化逻辑(如绑定按钮事件) +}; + const createChatLi = (content, className, targetBox = chatbox) => { const chatLi = document.createElement("li"); chatLi.classList.add("chat", `${className}`); @@ -64,17 +93,40 @@ const handleChat = () => { const incomingChatLi = createChatLi("Thinking...", "incoming", chatbox); const incomingRecordLi = createChatLi("Thinking...", "incoming", recordbox); // Add to recordbox - // timeout - const TIMEOUT_MS = CONFIG.TIME_OUT_SECOND; - let isTimeout = false; - const timeoutId = setTimeout(() => { - isTimeout = true; - incomingRecordLi.querySelector("p").textContent = "Request timed out. Please try again."; - }, TIMEOUT_MS); - // send request + // 超时逻辑优化 + let timeoutId; + const startTimeout = () => { + // 清除已有定时器(避免重复) + if (timeoutId) clearTimeout(timeoutId); + // 启动新定时器(使用同步后的 window.CONFIG.TIMEOUT_MS) + timeoutId = setTimeout(() => { + const timeoutMsg = ` + <div> + <p>请求超时(当前超时时间:${window.CONFIG.TIMEOUT_SECONDS || window.CONFIG.TIME_OUT_SECOND/1000}秒)</p> + <button class="retry-btn" style="margin-top:8px;padding:4px 8px;">点击重试</button> + </div> + `; + // 更新超时提示(带重试按钮) + incomingChatLi.querySelector("p").innerHTML = timeoutMsg; + incomingRecordLi.querySelector("p").textContent = `请求超时(${window.CONFIG.TIMEOUT_SECONDS || window.CONFIG.TIME_OUT_SECOND/1000}秒)`; + // 绑定重试事件(复用原有 generateResponse 逻辑) + incomingChatLi.querySelector(".retry-btn").addEventListener("click", () => { + incomingChatLi.querySelector("p").textContent = "Thinking..."; + incomingRecordLi.querySelector("p").textContent = "Thinking..."; + generateResponse(incomingChatLi, incomingRecordLi, () => { + clearTimeout(timeoutId); // 重试成功后清除超时 + }); + }); + }, window.CONFIG.TIMEOUT_MS || window.CONFIG.TIME_OUT_SECOND); + }; + + // 启动超时定时器 + startTimeout(); + + // 发送请求(原有逻辑,补充超时清除) generateResponse(incomingChatLi, incomingRecordLi, () => { - if (!isTimeout) clearTimeout(timeoutId); + clearTimeout(timeoutId); // 请求成功/失败时清除超时 }); } @@ -127,6 +179,24 @@ const generateResponse = (chatElement, recordElement, callback) => { accumulatedChatResponse += data.content; chatMessageElement.innerHTML = marked.parse(styleMatch(accumulatedChatResponse)); // Render Markdown chatbox.scrollTo(0, chatbox.scrollHeight); + + // 检查content中是否包含航班信息 + try { + // 尝试解析content中的JSON数据 + if (data.content.includes("flight_number")) { + // 提取JSON部分 + const jsonMatch = data.content.match(/\{[\s\S]*?\}/); + if (jsonMatch) { + const flightData = JSON.parse(jsonMatch[0]); + if (flightData.flight_number) { + // 将单个航班信息转换为数组格式 + renderFlightInfo([flightData]); + } + } + } + } catch (e) { + console.log("No valid flight info in content", e); + } } if (data.record) { accumulatedRecordResponse += data.record; @@ -162,29 +232,50 @@ const generateResponse = (chatElement, recordElement, callback) => { }); }; -// 渲染航班信息的函数,需要在generateResponse函数中当机票预定成功调用renderFlightInfo渲染,这里假如返回data.flightinfo -const renderFlightInfo = (flightInfo) => { +// 渲染航班信息的函数 +const renderFlightInfo = (flightInfos) => { + console.log("Rendering flight info:", flightInfos); const flightInfoContainer = document.getElementById('flight-info'); - // 清空提示信息 - flightInfoContainer.innerHTML = `<h3>航班信息</h3><p>正在加载航班信息...</p>`; - // 返回为空 - if (flightInfos.length === 0) { + // 清空容器并初始化基础结构(标题只显示一次) + flightInfoContainer.innerHTML = `<h3>航班信息</h3>`; + + // 判断参数(数组)是否为空 + if (!flightInfos || flightInfos.length === 0) { flightInfoContainer.innerHTML += "<p>没有航班信息可显示</p>"; return; } - flightInfos.forEach(flightInfo => { + + // 循环渲染每一条航班数据 + flightInfos.forEach(flight => { + // 确保flight是对象 + if (typeof flight === 'string') { + try { + flight = JSON.parse(flight); + } catch (e) { + console.error("Failed to parse flight info string:", e); + return; + } + } + const flightInfoHTML = ` - <h3>航班信息</h3> - <p>航班号: ${flightInfo.flightNumber}</p> - <p>乘客姓名: ${flightInfo.passengerName}</p> - <p>出发城市: ${flightInfo.departureCity}</p> - <p>到达城市: ${flightInfo.arrivalCity}</p> - <p>出发时间: ${flightInfo.departureTime}</p> - <p>到达时间: ${flightInfo.arrivalTime}</p> - <hr /> + <div class="flight-item"> + <p>航班号: ${flight.flight_number || '未知'}</p> + <p>乘客姓名: ${flight.passengerName || '未填写'}</p> + <p>出发城市: ${flight.origin || '未知'}</p> + <p>到达城市: ${flight.destination || '未知'}</p> + <p>出发时间: ${flight.departure_time || '未知'}</p> + <p>到达时间: ${flight.arrival_time || '未知'}</p> + <p>票价: ${flight.price || '未知'}</p> + <p>座位类型: ${flight.seat_type || '未知'}</p> + ${flight.message ? `<p class="success-message">状态: ${flight.message}</p>` : ''} + <hr /> + </div> `; flightInfoContainer.innerHTML += flightInfoHTML; }); + + // 显示航班信息区域 + flightInfoContainer.style.display = "block"; }; chatInput.addEventListener("input", () => { diff --git a/book-flight-ai-agent/go-client/frontend/static/style.css b/book-flight-ai-agent/go-client/frontend/static/style.css index a564d108..b2b2c2c3 100644 --- a/book-flight-ai-agent/go-client/frontend/static/style.css +++ b/book-flight-ai-agent/go-client/frontend/static/style.css @@ -37,6 +37,7 @@ body { transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease; + display: none; /* 默认不显示,只有在有航班信息时才显示 */ } #flight-info:hover { transform: translateY(5px); /* 悬停时上移 10px */ @@ -227,7 +228,7 @@ header h2 { padding: 15px 15px 15px 0; font-size: 0.95rem; } -.chat-input #send-btn +.chat-input #send-btn, .chat-input #add-btn { align-self: flex-end; color: #724ae8; diff --git a/book-flight-ai-agent/go-server/agents/cot_agent.go b/book-flight-ai-agent/go-server/agents/cot_agent.go index 08c25ad0..8d180e40 100644 --- a/book-flight-ai-agent/go-server/agents/cot_agent.go +++ b/book-flight-ai-agent/go-server/agents/cot_agent.go @@ -23,9 +23,7 @@ import ( "fmt" "strings" "time" -) -import ( "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/actions" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/conf" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/model" @@ -65,6 +63,12 @@ func (cot *CotAgentRunner) Run( callopt model.Option, callrst model.CallFunc, ) (string, error) { + // 1. 获取配置的超时时间(秒转毫秒) + timeoutSec := conf.GetEnvironment().TimeOut + timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second) + defer cancel() // 确保任务结束后释放资源 + + // 2. 将原有逻辑的 ctx 替换为 timeoutCtx(确保全链路受超时控制) timeNow := time.Now().Format("2006-01-02 15:04:05") opts := model.NewOptions(callopt) @@ -74,19 +78,32 @@ func (cot *CotAgentRunner) Run( var task string if len(cot.memoryMsg) > 0 { + // 传递 timeoutCtx 到 summaryIntent task = cot.summaryIntent(timeNow, callopt) } else { task = input } - // Runner + // Runner 循环(使用 timeoutCtx 判断超时) var response string var action actions.Action - var idxThoughtStep int32 var taskState TaskState + for idxThoughtStep < cot.maxThoughtSteps { - action, response = cot.thinkStep(task, timeNow, callopt, opts) + // 检查是否超时(提前退出循环) + select { + case <-timeoutCtx.Done(): + // 记录超时日志(包含任务内容、超时时间、步骤数) + // log.Printf( + // "Agent 任务超时:任务=%s,超时时间=%d秒,已执行步骤数=%d", + // task, timeoutSec, idxThoughtStep, + // ) + return fmt.Sprintf("任务超时(已超过%d秒)", timeoutSec), timeoutCtx.Err() + default: + } + // 传递 timeoutCtx 到 thinkStep/execAction + action, response = cot.thinkStep(timeoutCtx, task, timeNow, callopt, opts) taskState = InitTaskState(action.Method) observation := cot.execAction(action, opts) @@ -138,6 +155,7 @@ func (cot *CotAgentRunner) summaryIntent(timeNow string, callopt model.Option) s } func (cot *CotAgentRunner) thinkStep( + ctx context.Context, task string, now string, callopt model.Option, @@ -153,7 +171,7 @@ func (cot *CotAgentRunner) thinkStep( "format_instructions": conf.GetConfigPrompts().FormatInstructions, }, ) - response, _ := cot.llm.Invoke(context.Background(), prompt, callopt, ollama.WithTemperature(0.0)) + response, _ := cot.llm.Invoke(ctx, prompt, callopt, ollama.WithTemperature(0.0)) opts.CallOpt("\n") response = model.RemoveThink(response) return actions.NewAction(response), response diff --git a/book-flight-ai-agent/go-server/cmd/server.go b/book-flight-ai-agent/go-server/cmd/server.go index 7cca7dd2..5dd10def 100644 --- a/book-flight-ai-agent/go-server/cmd/server.go +++ b/book-flight-ai-agent/go-server/cmd/server.go @@ -34,6 +34,8 @@ import ( import ( "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/agents" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/conf" + "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/model" + "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/model/bailian" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/model/ollama" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/tools" "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/tools/bookingflight" @@ -65,12 +67,24 @@ func getTools() tools.Tools { } type ChatServer struct { - llm *ollama.LLMOllama + llm model.LLM cot agents.CotAgentRunner } func NewChatServer() (*ChatServer, error) { - llm := ollama.NewLLMOllama(cfgEnv.Model, cfgEnv.Url) + var llm model.LLM + + // 检查URL是否包含dashscope.aliyuncs.com,判断是否使用百炼API + if strings.Contains(cfgEnv.Url, "dashscope.aliyuncs.com") { + // 使用百炼API + log.Println("使用百炼API") + llm = bailian.NewLLMBailian(cfgEnv.Model, cfgEnv.Url, cfgEnv.ApiKey) + } else { + // 使用Ollama API + log.Println("使用Ollama API") + llm = ollama.NewLLMOllama(cfgEnv.Model, cfgEnv.Url) + } + cot := agents.NewCotAgentRunner(llm, getTools(), 10, conf.GetConfigPrompts()) return &ChatServer{llm: llm, cot: cot}, nil } diff --git a/book-flight-ai-agent/go-server/conf/config.go b/book-flight-ai-agent/go-server/conf/config.go index d9102dff..baf29d01 100644 --- a/book-flight-ai-agent/go-server/conf/config.go +++ b/book-flight-ai-agent/go-server/conf/config.go @@ -65,7 +65,7 @@ func loadEnvironment() { configEnv.PortClient = AtoiWithDefault("CLIENT_PORT", 20000) configEnv.UrlClient = fmt.Sprintf("%s:%d", configEnv.HostClient, configEnv.PortClient) configEnv.PortWeb = AtoiWithDefault("WEB_PORT", 8080) - configEnv.TimeOut = AtoiWithDefault("TIMEOUT_SECONDS", 300) + configEnv.TimeOut = AtoiWithDefault("TIMEOUT_SECONDS", 120) // 默认 120 秒(2分钟) } func GetEnvironment() Environment { diff --git a/book-flight-ai-agent/go-server/model/bailian/bailian.go b/book-flight-ai-agent/go-server/model/bailian/bailian.go new file mode 100644 index 00000000..0193df23 --- /dev/null +++ b/book-flight-ai-agent/go-server/model/bailian/bailian.go @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bailian + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +import ( + "github.com/apache/dubbo-go-samples/book-flight-ai-agent/go-server/model" +) + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type BailianRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` +} + +type BailianResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +type LLMBailian struct { + Model string + Url string + ApiKey string + MaxTokens int + options []any +} + +func NewLLMBailian(model string, url string, apiKey string) *LLMBailian { + return &LLMBailian{ + Model: model, + Url: url, + ApiKey: apiKey, + MaxTokens: 2048, + options: []any{}, + } +} + +func (llm *LLMBailian) Call(ctx context.Context, input string, opts ...model.Option) (string, error) { + return llm.Invoke(ctx, input, opts...) +} + +func (llm *LLMBailian) Stream(ctx context.Context, input string, opts ...model.Option) (string, error) { + // 百炼API的流式调用实现 + // 简化版本,实际上应该使用流式API + return llm.Invoke(ctx, input, opts...) +} + +func (llm *LLMBailian) Invoke(ctx context.Context, input string, opts ...model.Option) (string, error) { + options := model.NewOptions(opts...) + + // 解析选项 + temperature := 0.7 + for _, opt := range llm.options { + if temp, ok := opt.(WithTemperature); ok { + temperature = float64(temp) + } + } + + // 构建请求体 + reqBody := BailianRequest{ + Model: llm.Model, + Messages: []Message{ + {Role: "user", Content: input}, + }, + Temperature: temperature, + MaxTokens: llm.MaxTokens, + Stream: false, + } + + // 将请求体转换为JSON + jsonData, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %v", err) + } + + // 创建HTTP请求 + req, err := http.NewRequestWithContext(ctx, "POST", llm.Url+"/chat/completions", bytes.NewBuffer(jsonData)) + if err != nil { + return "", fmt.Errorf("failed to create request: %v", err) + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+llm.ApiKey) + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + // 读取响应体 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %v", err) + } + + // 检查响应状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody)) + } + + // 解析响应 + var bailianResp BailianResponse + if err := json.Unmarshal(respBody, &bailianResp); err != nil { + return "", fmt.Errorf("failed to unmarshal response: %v", err) + } + + // 检查是否有结果 + if len(bailianResp.Choices) == 0 { + return "", fmt.Errorf("no completion choices returned") + } + + // 获取结果 + result := bailianResp.Choices[0].Message.Content + result = strings.TrimSpace(result) + + // 调用回调函数(如果有) + if options.CallOpt != nil { + options.CallOpt(result) + } + + return result, nil +} \ No newline at end of file diff --git a/book-flight-ai-agent/go-server/model/bailian/options.go b/book-flight-ai-agent/go-server/model/bailian/options.go new file mode 100644 index 00000000..c793320e --- /dev/null +++ b/book-flight-ai-agent/go-server/model/bailian/options.go @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bailian + +type WithTemperature float32 + +func (w WithTemperature) Apply(opts *[]any) { + *opts = append(*opts, w) +} \ No newline at end of file diff --git a/book-flight-ai-agent/go-server/model/ollama/ollama.go b/book-flight-ai-agent/go-server/model/ollama/ollama.go index b583a32f..5bf51a50 100644 --- a/book-flight-ai-agent/go-server/model/ollama/ollama.go +++ b/book-flight-ai-agent/go-server/model/ollama/ollama.go @@ -18,6 +18,7 @@ package ollama import ( "context" + "fmt" "log" "net/http" "net/url" @@ -100,11 +101,22 @@ func NewLLMOllama(model string, url string) *LLMOllama { } } +func (llm *LLMOllama) Stream(ctx context.Context, input string, opts ...model.Option) (string, error) { + return llm.Call(ctx, input, opts...) +} + func (llm *LLMOllama) Call(ctx context.Context, input string, opts ...model.Option) (string, error) { - client := api.NewClient(&url.URL{Scheme: llm.llmUrl.scheam, Host: llm.llmUrl.host}, http.DefaultClient) + // 检查 ctx 是否已超时(提前返回) + select { + case <-ctx.Done(): + return "", fmt.Errorf("LLM 调用超时:%w", ctx.Err()) + default: + } + client := api.NewClient(&url.URL{Scheme: llm.llmUrl.scheam, Host: llm.llmUrl.host}, http.DefaultClient) optss := model.NewOptions(opts...) + // 传递 ctx 到 client.Generate(确保 LLM 调用受超时控制) // By default, GenerateRequest is streaming. req := &api.GenerateRequest{ Model: llm.Model, @@ -114,26 +126,28 @@ func (llm *LLMOllama) Call(ctx context.Context, input string, opts ...model.Opti Options: optss.Opts, } - var respBuilder strings.Builder // Use strings.Builder + var respBuilder strings.Builder respFunc := func(resp api.GenerateResponse) error { respBuilder.WriteString(resp.Response) return optss.CallOpt(resp.Response) } + // 使用带超时的 ctx 调用 LLM err := client.Generate(ctx, req, respFunc) if err != nil { - log.Fatal(err) + log.Printf("LLM 调用失败(可能超时):%v", err) return "", err } return respBuilder.String(), nil } -func (llm *LLMOllama) Stream(ctx context.Context, input string, opts ...model.Option) (string, error) { - return llm.Call(ctx, input, opts...) -} - func (llm *LLMOllama) Invoke(ctx context.Context, input string, opts ...model.Option) (string, error) { + select { + case <-ctx.Done(): + return "", fmt.Errorf("LLM Invoke 超时:%w", ctx.Err()) + default: + } client := api.NewClient(&url.URL{Scheme: llm.llmUrl.scheam, Host: llm.llmUrl.host}, http.DefaultClient) // Messages
