You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2025/09/04 10:17:21 UTC

(dubbo-go-samples) branch book-fight created (now b24d4815)

This is an automated email from the ASF dual-hosted git repository.

alexstocks pushed a change to branch book-fight
in repository https://gitbox.apache.org/repos/asf/dubbo-go-samples.git


      at b24d4815 update book-flight

This branch includes the following new commits:

     new b24d4815 update book-flight

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(dubbo-go-samples) 01/01: update book-flight

Posted by al...@apache.org.
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 <al...@foxmail.com>
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