虚拟聊天项目中的思维链实践:从简化版 COT/TOT 到标准实现的探索

摘要:本文基于个人学习项目 virtual-chat,分享了在实现思维链(Chain of Thought, COT)和思维树(Tree of Thoughts, TOT)功能时的实践经验。文章详细介绍了基于单次调用的简化版实现方案、遇到的技术问题及解决方案,并深入探讨了真正的多阶段 COT/TOT 实现架构。通过对比两种方案的优劣,为读者提供了一套完整的思维链功能实现思路和技术选型参考。

关键词:思维链;COT;TOT;大模型;流式输出;Spring Boot;React;Redis


1. 引言

随着大语言模型(LLM)的快速发展,如何让 AI 更好地展示其推理过程成为了一个重要课题。OpenAI 提出的 Chain of Thought(思维链)Tree of Thoughts(思维树) 技术,通过引导模型展示中间推理步骤,显著提升了复杂问题的解决能力。

在实际项目中,我们面临着这样的选择:

  • 简化版实现:成本低、速度快,但推理质量有限
  • 标准版实现:质量高、可追溯,但成本高、实现复杂

本文以个人学习项目 virtual-chat 为例,分享从简化版到标准版的完整探索过程,希望能为有类似需求的开发者提供参考。


2. 项目背景

2.1 项目介绍

virtual-chat 是一个基于 Spring Boot + React 的虚拟聊天应用,支持用户与自定义的"虚拟朋友"进行对话。项目的核心特色是提供了三种对话模式:

  1. 常规模式:直接对话,无思维过程展示
  2. COT 模式(Chain of Thought):展示逐步推理的思维链过程
  3. TOT 模式(Tree of Thoughts):展示多种思路探索和比较的思维树过程

2.2 技术栈

  • 后端:Spring Boot 3.2.0 + Spring AI Alibaba(通义千问)
  • 前端:React + TypeScript + Vite
  • 数据库:H2(开发环境)/ MySQL(生产环境)
  • 缓存:Redis(用于存储中间结果)
  • 通信:Server-Sent Events(SSE)实现流式输出

2.3 功能需求

  • ✅ 支持三种对话模式切换
  • ✅ 思维链内容实时流式展示
  • ✅ 思维过程和最终回答分离显示
  • ✅ 历史消息持久化和回显
  • ✅ 可折叠的思维链容器

3. 简化版 COT/TOT 实现

3.1 核心思路

简化版的核心思想是:只调用一次大模型,通过精心设计的提示词(Prompt),让 AI 在单次响应中同时输出思维过程和最终答案

这种方式的优势非常明显:

  • 🚀 响应速度快:只需一次网络请求
  • 💰 Token 消耗少:成本约为标准版的 1/2 - 1/3
  • 🔧 实现简单:代码量少,维护成本低

3.2 后端实现

3.2.1 提示词设计

根据 COT 和 TOT 的不同特点,设计了不同的提示词模板:

// StreamingChatService.java

if ("COT".equals(thinkingChainMode)) {
    fullPrompt += "\n\n【重要】请严格按照以下格式输出(使用XML标签):\n" +
                 "1. 首先输出 <think>\n" +
                 "2. 然后展示你的逐步思考过程(思维链)\n" +
                 "3. 输出 </think>\n" +
                 "4. 输出 <answer>\n" +
                 "5. 给出简洁的最终回答\n" +
                 "6. 输出 </answer>\n" +
                 "\n示例:\n" +
                 "<think>\n" +
                 "让我来分析这个问题...首先...其次...\n" +
                 "</think>\n" +
                 "<answer>\n" +
                 "这是最终答案。\n" +
                 "</answer>";
} else if ("TOT".equals(thinkingChainMode)) {
    fullPrompt += "\n\n【重要】请严格按照以下格式输出(使用XML标签):\n" +
                 "1. 首先输出 <think>\n" +
                 "2. 然后展示你的多种思路和方案比较(思维树)\n" +
                 "3. 输出 </think>\n" +
                 "4. 输出 <answer>\n" +
                 "5. 给出最佳方案的简洁回答\n" +
                 "6. 输出 </answer>\n" +
                 "\n示例:\n" +
                 "<think>\n" +
                 "方案A:... 方案B:... 方案C:... 经过比较,方案B最优...\n" +
                 "</think>\n" +
                 "<answer>\n" +
                 "基于方案B的最终答案。\n" +
                 "</answer>";
}

关键点

  • 使用 XML 标签(<think></think><answer></answer>)作为分隔符
  • 提供清晰的示例,引导 AI 按照指定格式输出
  • COT 强调"逐步思考",TOT 强调"多种方案比较"
3.2.2 流式输出
return chatModel.stream(fullPrompt)
    .doOnNext(text -> System.out.println("[流式聊天] AI输出chunk: " + text))
    .filter(t -> t != null && !t.trim().isEmpty());

使用 Reactor Flux 实现流式输出,每个 chunk 实时推送到前端。

3.3 前端实现

3.3.1 SSE 数据接收与解析

前端通过 EventSource 接收 SSE 数据流,并根据 XML 标记进行解析:

// chat.html

let isInThinking = false;
let isInAnswer = false;
let thinkingContent = '';
let answerContent = '';
let markerBuffer = ''; // 用于检测跨 chunk 的标记

eventSource.onmessage = function(event) {
    const chunk = event.data;
    
    // 累积缓冲区以检测跨 chunk 的标记
    markerBuffer += chunk;
    
    // 检测思维链开始标记
    if (markerBuffer.includes('<think>') && !isInThinking) {
        isInThinking = true;
        markerBuffer = markerBuffer.replace('<think>', '');
        createThinkingContainer(); // 创建思维链容器
        return;
    }
    
    // 检测思维链结束标记
    if (markerBuffer.includes('</think>') && isInThinking) {
        isInThinking = false;
        markerBuffer = markerBuffer.replace('</think>', '');
        // 将累积的内容作为思维过程保存
        return;
    }
    
    // 检测答案开始标记
    if (markerBuffer.includes('<answer>') && !isInAnswer) {
        isInAnswer = true;
        markerBuffer = markerBuffer.replace('<answer>', '');
        createAnswerContainer(); // 创建答案容器
        return;
    }
    
    // 检测答案结束标记
    if (markerBuffer.includes('</answer>') && isInAnswer) {
        isInAnswer = false;
        markerBuffer = markerBuffer.replace('</answer>', '');
        // 保存消息到数据库
        saveAIMessage(answerContent, thinkingContent);
        return;
    }
    
    // 根据当前状态追加内容
    if (isInThinking) {
        thinkingContent += chunk;
        appendToThinkingContainer(chunk);
    } else if (isInAnswer) {
        answerContent += chunk;
        appendToAnswerContainer(chunk);
    }
};
3.3.2 思维链容器创建
function createThinkingContainer() {
    const thinkingContainer = document.createElement('div');
    thinkingContainer.className = 'thinking-chain-container';
    thinkingContainer.innerHTML = `
        <div class="thinking-header" onclick="toggleThinking(this)">
            <i class="bi bi-lightbulb"></i> 思维链推理过程
            <span class="thinking-toggle"><i class="bi bi-chevron-down"></i></span>
        </div>
        <div class="thinking-content streaming-cursor"></div>
    `;
    messageContainer.appendChild(thinkingContainer);
}

function createAnswerContainer() {
    const answerContainer = document.createElement('div');
    answerContainer.className = 'answer-container';
    answerContainer.innerHTML = `
        <div class="answer-label">
            <i class="bi bi-check-circle"></i> 最终回答
        </div>
        <div class="answer-content streaming-cursor"></div>
    `;
    messageContainer.appendChild(answerContainer);
}
3.3.3 可折叠交互
function toggleThinking(header) {
    const content = header.nextElementSibling;
    const toggleIcon = header.querySelector('.thinking-toggle i');
    
    if (content.classList.contains('collapsed')) {
        content.classList.remove('collapsed');
        toggleIcon.className = 'bi bi-chevron-up';
    } else {
        content.classList.add('collapsed');
        toggleIcon.className = 'bi bi-chevron-down';
    }
}

思维完成后自动折叠,用户可以点击展开查看详细推理过程。

3.4 数据持久化

3.4.1 数据库设计
CREATE TABLE message (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    friend_id BIGINT NOT NULL,
    sender VARCHAR(10) NOT NULL, -- 'user' or 'friend'
    content TEXT NOT NULL,
    thinking_content TEXT, -- 思维链内容(COT/TOT模式)
    timestamp DATETIME NOT NULL
);
3.4.2 保存逻辑
async function saveAIMessage(content, thinkingContent = null) {
    try {
        const requestBody = {
            friendId: currentFriendId,
            content: content
        };
        
        if (thinkingContent) {
            requestBody.thinkingContent = thinkingContent;
        }
        
        await fetch('/api/messages/save', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(requestBody)
        });
    } catch (error) {
        console.error('保存AI消息失败:', error);
    }
}
3.4.3 历史消息回显
// 加载历史消息时,如果有思维链内容,特殊渲染
} else if (msg.sender === 'friend' && msg.thinkingContent) {
    contentHtml = `
        <div class="thinking-chain-container">
            <div class="thinking-header" onclick="toggleThinking(this)">
                <i class="bi bi-lightbulb"></i> 思维链推理过程
                <span class="thinking-toggle"><i class="bi bi-chevron-down"></i></span>
            </div>
            <div class="thinking-content collapsed">${msg.thinkingContent}</div>
        </div>
        <div class="answer-container">
            <div class="answer-label">
                <i class="bi bi-check-circle"></i> 最终回答
            </div>
            <div class="answer-content">${msg.content}</div>
        </div>
    `;
}

4. 遇到的问题及解决方案

在实现过程中,遇到了多个技术问题,以下是主要问题及解决方案:

4.1 问题一:data: 前缀和空白行显示

问题描述
思维链内容和最终回答中显示了 “data:” 前缀和大量空白行,影响阅读体验。

根本原因
SSE 协议中,Spring Boot 会自动为每个 chunk 添加 data: 前缀(无空格)。前端正则表达式 /^data:\s*/g 缺少 m(multiline)标志,导致 ^ 只匹配字符串开头,而不是每行开头。

解决方案

// 修复前
const cleaned = beforeMarker.replace(/^data:\s*/g, '');

// 修复后
const cleaned = beforeMarker
    .replace(/^data:\s*/gm, '')  // 添加 m 标志,匹配每行开头
    .split('\n')
    .filter(line => line.trim() !== '')  // 过滤空白行
    .join('\n');

效果:清理后的内容干净整洁,无多余前缀和空白行。

4.2 问题二:思维链内容未持久化

问题描述
刷新页面后,思维链内容没有回显,只有最终回答显示。

根本原因

  1. 初始实现中,saveAIMessage 调用的是 /api/messages POST 接口,该接口会重新生成 AI 回复,而不是保存前端已收到的流式内容
  2. 保存条件 if (isThinkingMode && isInAnswer) 依赖 isInAnswer 状态,如果 AI 没有正确输出 <answer> 标记,就不会保存

解决方案

  1. 新增专用保存接口
// MessageController.java
@PostMapping("/save")
public ResponseEntity<?> saveMessage(@RequestBody Map<String, Object> request, HttpSession session) {
    User user = (User) session.getAttribute("loggedInUser");
    if (user == null) {
        return ResponseEntity.status(401).body(Map.of("error", "未登录"));
    }
    
    Long friendId = Long.valueOf(request.get("friendId").toString());
    String content = (String) request.get("content");
    String thinkingContent = (String) request.get("thinkingContent");
    
    Message aiMessage = new Message();
    aiMessage.setFriendId(friendId);
    aiMessage.setSender("friend");
    aiMessage.setContent(content);
    
    if (thinkingContent != null && !thinkingContent.isEmpty()) {
        aiMessage.setThinkingContent(thinkingContent);
    }
    
    aiMessage.setTimestamp(LocalDateTime.now());
    messageRepository.save(aiMessage);
    
    return ResponseEntity.ok(Map.of("success", true, "messageId", aiMessage.getId()));
}
  1. 改进保存条件
// 修复前
if (isThinkingMode && isInAnswer) {
    await saveAIMessage(answerContent, thinkingContent);
}

// 修复后
if (isThinkingMode && (thinkingContent || answerContent)) {
    console.log('=== 准备保存消息 ===');
    await saveAIMessage(answerContent || thinkingContent, thinkingContent);
}

效果:只要有内容就保存,确保消息不会丢失。

4.3 问题三:toggleThinking 函数报错

问题描述
控制台报错 Uncaught TypeError: Cannot read properties of null (reading 'classList'),导致后续的保存逻辑无法执行。

根本原因
toggleThinking 函数期望 .thinking-toggle 元素存在,但流式输出时创建思维链容器的 HTML 中缺少这个元素。

// toggleThinking 函数
function toggleThinking(header) {
    const toggleIcon = header.querySelector('.thinking-toggle i'); // 返回 null
    toggleIcon.className = 'bi bi-chevron-up'; // 报错!
}

解决方案

在思维链容器的 HTML 中添加缺失的元素:

thinkingContainer.innerHTML = `
    <div class="thinking-header" onclick="toggleThinking(this)">
        <i class="bi bi-lightbulb"></i> 思维链推理过程
        <span class="thinking-toggle"><i class="bi bi-chevron-down"></i></span>
    </div>
    <div class="thinking-content streaming-cursor"></div>
`;

效果:函数正常执行,不再报错,保存逻辑顺利执行。

4.4 问题四:标记被拆分到多个 chunk

问题描述
由于 SSE 流式输出的特性,XML 标记(如 <think>)可能被拆分到多个 chunk 中,导致无法正确检测。

解决方案

使用缓冲区累积数据,检测完整标记后再处理:

let markerBuffer = '';

eventSource.onmessage = function(event) {
    const chunk = event.data;
    markerBuffer += chunk; // 累积到缓冲区
    
    // 检测完整标记
    if (markerBuffer.includes('<think>')) {
        // 处理标记
        markerBuffer = markerBuffer.replace('<think>', ''); // 移除已处理的标记
    }
};

效果:即使标记被拆分,也能正确检测和解析。


5. 简化版的局限性

虽然简化版实现具有成本低、速度快的优势,但也存在明显的局限性:

5.1 不是真正的多步推理

简化版只是让 AI "假装"在思考,实际上是一次性生成的内容。AI 并没有真正经历"先推理、后总结"的过程,可能导致:

  • 思考深度不足
  • 逻辑跳跃
  • 表面化的推理过程

5.2 TOT 没有真正的树状探索

TOT 模式的本质是树状搜索:生成多个分支 → 评估每个分支 → 选择最优路径。但简化版只是让 AI 列出多个方案,没有真正的评估和选择过程。

5.3 不可追溯

中间推理过程无法单独查询或重用,所有信息都混合在一次响应中。

5.4 提示词依赖性强

推理质量高度依赖提示词的设计,如果 AI 不严格按照格式输出,前端解析就会失败。


6. 真正的 COT/TOT 实现探讨

基于简化版的实践经验,我们来探讨如何实现真正的多阶段 COT/TOT

6.1 核心设计理念

真正的 COT/TOT 实现应该具备以下特点:

  1. 多次调用大模型:每个阶段独立调用,专注不同任务
  2. 中间结果存储:使用 Redis 缓存各阶段的输出
  3. 分阶段展示:前端用标签页分别展示不同阶段的内容
  4. 流式输出:每个阶段都支持实时流式展示
  5. 错误处理:某个阶段失败时,支持重试或降级

6.2 COT 标准实现(两阶段)

阶段 1:思维链推理
用户提问
  ↓
第一次大模型调用(推理阶段)
  Prompt: "请逐步分析这个问题,展示你的推理过程"
  ↓
Redis 存储:key = "cot:{sessionId}:step1", value = 推理过程
  ↓
前端显示:【🧠 思维链】标签页实时展示推理过程(流式输出)

代码示例

@Service
public class StandardCOTService {
    
    @Autowired
    private DashScopeChatModel chatModel;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public Flux<String> executeCOT(Long friendId, String userMessage, String sessionId) {
        // 阶段 1:推理
        String reasoningPrompt = buildReasoningPrompt(friendId, userMessage);
        StringBuilder reasoningContent = new StringBuilder();
        
        return chatModel.stream(reasoningPrompt)
            .doOnNext(chunk -> {
                reasoningContent.append(chunk);
                // 实时推送到前端【思维链】标签
                sendToClient("thinking", chunk);
            })
            .collectList()
            .flatMapMany(chunks -> {
                String reasoning = String.join("", chunks);
                
                // 存储到 Redis
                String redisKey = "cot:" + sessionId + ":step1";
                redisTemplate.opsForValue().set(redisKey, reasoning, 1, TimeUnit.HOURS);
                
                // 阶段 2:生成答案
                String answerPrompt = "基于以下推理过程,给出简洁的最终答案:\n" + reasoning;
                
                return chatModel.stream(answerPrompt)
                    .doOnNext(chunk -> {
                        // 实时推送到前端【最终回答】标签
                        sendToClient("answer", chunk);
                    });
            });
    }
}
阶段 2:生成最终答案
读取 Redis 中的 step1 内容
  ↓
第二次大模型调用(答案阶段)
  Prompt: "基于以下推理过程,给出简洁的最终答案:{step1}"
  ↓
Redis 存储:key = "cot:{sessionId}:step2", value = 最终答案
  ↓
前端显示:【✅ 最终回答】标签页展示答案(流式输出)

6.3 TOT 标准实现(三阶段)

阶段 1:生成多个思路(发散)
用户提问
  ↓
第一次大模型调用(发散阶段)
  Prompt: "请给出 3 种不同的解决方案,分别标注为方案A、方案B、方案C"
  ↓
Redis 存储:key = "tot:{sessionId}:step1", value = JSON数组
  ↓
前端显示:【🌳 思路探索】标签页展示 3 个方案
阶段 2:评估各方案(评估)
读取 Redis 中的 step1 内容
  ↓
第二次大模型调用(评估阶段)
  Prompt: "请评估以下方案的优缺点:{solutions}"
  ↓
Redis 存储:key = "tot:{sessionId}:step2", value = JSON对象
  ↓
前端显示:【📊 方案评估】标签页展示评估结果
阶段 3:选择最佳并回答(收敛)
读取 Redis 中的 step1 + step2 内容
  ↓
第三次大模型调用(决策阶段)
  Prompt: "基于以上方案和评估,选择最优方案并给出最终答案"
  ↓
Redis 存储:key = "tot:{sessionId}:step3", value = 最终答案
  ↓
前端显示:【✅ 最终回答】标签页展示答案

代码示例

@Service
public class StandardTOTService {
    
    @Autowired
    private DashScopeChatModel chatModel;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public Flux<String> executeTOT(Long friendId, String userMessage, String sessionId) {
        // 阶段 1:生成多个方案
        String explorationPrompt = buildExplorationPrompt(friendId, userMessage);
        
        return chatModel.stream(explorationPrompt)
            .collectList()
            .flatMapMany(chunks -> {
                String solutions = String.join("", chunks);
                
                // 存储到 Redis
                String redisKey1 = "tot:" + sessionId + ":step1";
                redisTemplate.opsForValue().set(redisKey1, solutions, 1, TimeUnit.HOURS);
                
                // 阶段 2:评估方案
                String evaluationPrompt = "请评估以下方案的优缺点:\n" + solutions;
                
                return chatModel.stream(evaluationPrompt)
                    .collectList()
                    .flatMapMany(evalChunks -> {
                        String evaluations = String.join("", evalChunks);
                        
                        // 存储到 Redis
                        String redisKey2 = "tot:" + sessionId + ":step2";
                        redisTemplate.opsForValue().set(redisKey2, evaluations, 1, TimeUnit.HOURS);
                        
                        // 阶段 3:选择最佳方案并回答
                        String decisionPrompt = "基于以下方案和评估,选择最优方案并给出最终答案:\n" +
                                              "方案:\n" + solutions + "\n\n" +
                                              "评估:\n" + evaluations;
                        
                        return chatModel.stream(decisionPrompt);
                    });
            });
    }
}

6.4 前端标签页设计

COT 模式前端结构
<div class="thinking-chain-container">
    <!-- 标签页导航 -->
    <div class="tabs">
        <div class="tab active" data-tab="thinking" onclick="switchTab('thinking')">
            <i class="bi bi-lightbulb"></i> 🧠 思维链
        </div>
        <div class="tab" data-tab="answer" onclick="switchTab('answer')">
            <i class="bi bi-check-circle"></i> ✅ 最终回答
        </div>
    </div>
    
    <!-- 标签页内容 -->
    <div class="tab-content active" id="tab-thinking">
        <div class="content streaming-cursor"></div>
    </div>
    <div class="tab-content" id="tab-answer" style="display:none">
        <div class="content streaming-cursor"></div>
    </div>
</div>
TOT 模式前端结构
<div class="thinking-tree-container">
    <!-- 标签页导航 -->
    <div class="tabs">
        <div class="tab active" data-tab="exploration" onclick="switchTab('exploration')">
            <i class="bi bi-diagram-3"></i> 🌳 思路探索
        </div>
        <div class="tab" data-tab="evaluation" onclick="switchTab('evaluation')">
            <i class="bi bi-bar-chart"></i> 📊 方案评估
        </div>
        <div class="tab" data-tab="answer" onclick="switchTab('answer')">
            <i class="bi bi-check-circle"></i> ✅ 最终回答
        </div>
    </div>
    
    <!-- 标签页内容 -->
    <div class="tab-content active" id="tab-exploration">
        <div class="content streaming-cursor"></div>
    </div>
    <div class="tab-content" id="tab-evaluation" style="display:none">
        <div class="content streaming-cursor"></div>
    </div>
    <div class="tab-content" id="tab-answer" style="display:none">
        <div class="content streaming-cursor"></div>
    </div>
</div>
JavaScript 标签切换逻辑
function switchTab(tabName) {
    // 移除所有标签的 active 状态
    document.querySelectorAll('.tab').forEach(tab => {
        tab.classList.remove('active');
    });
    document.querySelectorAll('.tab-content').forEach(content => {
        content.style.display = 'none';
        content.classList.remove('active');
    });
    
    // 激活选中的标签
    document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
    document.getElementById(`tab-${tabName}`).style.display = 'block';
    document.getElementById(`tab-${tabName}`).classList.add('active');
}

// SSE 数据接收时,根据阶段推送到对应标签
eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    
    if (data.stage === 'thinking' || data.stage === 'exploration') {
        appendToTab('tab-thinking', data.content);
    } else if (data.stage === 'evaluation') {
        appendToTab('tab-evaluation', data.content);
    } else if (data.stage === 'answer') {
        appendToTab('tab-answer', data.content);
    }
};

6.5 SSE 流式输出协议

标准版需要扩展 SSE 数据格式,增加阶段标识:

// 阶段 1 输出
{
  "stage": "thinking",
  "content": "让我来分析这个问题...",
  "timestamp": 1715234567890
}

// 阶段 2 输出
{
  "stage": "answer",
  "content": "最终答案是...",
  "timestamp": 1715234568901
}

// 阶段完成信号
{
  "stage": "complete",
  "sessionId": "abc123",
  "totalStages": 2,
  "completedStages": 2
}

6.6 Redis 存储策略

Key 设计规范
COT 模式:
- cot:{sessionId}:step1 → 推理过程(String)
- cot:{sessionId}:step2 → 最终答案(String)

TOT 模式:
- tot:{sessionId}:step1 → 多个方案(JSON 字符串)
- tot:{sessionId}:step2 → 评估结果(JSON 字符串)
- tot:{sessionId}:step3 → 最终答案(String)
过期时间设置
// 建议 1 小时过期,平衡成本和可用性
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
数据结构示例
// TOT step1 - 方案列表
{
  "方案A": "使用递归算法解决...",
  "方案B": "使用动态规划解决...",
  "方案C": "使用贪心算法解决..."
}

// TOT step2 - 评估结果
{
  "方案A": {
    "优点": ["实现简单", "代码可读性好"],
    "缺点": ["性能较差", "可能栈溢出"]
  },
  "方案B": {
    "优点": ["性能最优", "避免重复计算"],
    "缺点": ["实现复杂", "空间占用大"]
  }
}

7. 两种方案对比

维度 简化版(当前) 标准版(多阶段)
调用次数 1 次 COT: 2 次 / TOT: 3 次
推理质量 表面思考 深度推理
中间结果存储 Redis 持久化(1小时过期)
前端展示 单一容器,用 XML 标记分隔 多标签页,分阶段展示
可追溯性 低(无法单独查询中间过程) 高(每步都可查)
Token 消耗 少(基准) 多(2-3 倍)
响应速度 快(单次调用) 慢(串行调用)
实现复杂度 简单(~100 行代码) 复杂(~500 行代码)
维护成本
错误处理 简单 复杂(需重试/降级)
用户体验 流畅(实时流式) 需进度提示(等待时间长)
适用场景 简单问题、成本敏感 复杂问题、质量优先

8. 性能优化策略

8.1 并行化(仅适用于 TOT 阶段 1)

TOT 阶段 1 可以并行生成多个方案,减少等待时间:

CompletableFuture<String> solutionA = chatModel.asyncStream(promptA);
CompletableFuture<String> solutionB = chatModel.asyncStream(promptB);
CompletableFuture<String> solutionC = chatModel.asyncStream(promptC);

CompletableFuture.allOf(solutionA, solutionB, solutionC).join();

8.2 缓存复用

如果相同问题已存在 Redis,直接返回历史结果:

String cachedResult = redisTemplate.opsForValue().get("cot:" + questionHash);
if (cachedResult != null) {
    return Flux.just(cachedResult);
}

8.3 超时控制

每个阶段设置超时时间,避免长时间等待:

return chatModel.stream(prompt)
    .timeout(Duration.ofSeconds(30))
    .onErrorResume(TimeoutException.class, e -> {
        log.warn("阶段超时,使用降级方案");
        return Flux.just("抱歉,思考超时,请稍后重试");
    });

8.4 重试机制

某个阶段失败时,自动重试(最多 3 次):

private Flux<String> retryStage(String prompt, int maxRetries) {
    return chatModel.stream(prompt)
        .retryWhen(Retry.backoff(maxRetries, Duration.ofSeconds(1))
            .filter(throwable -> !(throwable instanceof TimeoutException)));
}

8.5 降级方案

如果多阶段推理失败,回退到简化版:

try {
    return executeStandardCOT(friendId, userMessage, sessionId);
} catch (Exception e) {
    log.warn("标准版失败,降级到简化版", e);
    return executeSimpleCOT(friendId, userMessage);
}

9. 监控和日志

9.1 阶段耗时统计

long startTime = System.currentTimeMillis();

chatModel.stream(prompt)
    .doOnComplete(() -> {
        long duration = System.currentTimeMillis() - startTime;
        log.info("阶段 {} 耗时: {} ms", stageName, duration);
    });

9.2 Token 消耗统计

int totalTokens = 0;

chatModel.stream(prompt)
    .doOnNext(chunk -> {
        int chunkTokens = estimateTokens(chunk);
        totalTokens += chunkTokens;
    })
    .doOnComplete(() -> {
        log.info("阶段 {} Token 消耗: {}", stageName, totalTokens);
    });

9.3 异常情况记录

chatModel.stream(prompt)
    .onErrorResume(e -> {
        log.error("阶段 {} 异常: {}", stageName, e.getMessage(), e);
        metricsRecorder.recordError(stageName, e.getClass().getSimpleName());
        return Flux.empty();
    });

10. 总结与建议

10.1 当前状态

virtual-chat 项目目前使用的是简化版实现

  • 单次大模型调用
  • 通过提示词让 AI 模拟思考过程
  • 前端使用 XML 标记解析和展示
  • 优点:快速、低成本、易维护
  • 缺点:推理质量有限

10.2 未来方向

如果需要提升推理质量,可以考虑改造为标准版实现

  • 多次大模型调用(COT: 2 次,TOT: 3 次)
  • 每个阶段专注不同任务
  • 中间结果存储在 Redis
  • 前端分标签页展示
  • 优点:推理质量高、可追溯、可扩展
  • 缺点:成本高、响应慢、实现复杂

10.3 实施建议

  1. 短期:保持当前简化版实现,满足个人学习需求
  2. 中期:如果发现问题,可以逐步优化提示词,提升推理质量
  3. 长期:如果有更高要求,再考虑改造为标准版实现

10.4 技术选型建议

场景 推荐方案 理由
个人学习项目 简化版 成本低、实现简单
企业内部助手 简化版 + 优化提示词 平衡成本和质量
专业咨询系统 标准版 推理质量优先
学术研究平台 标准版 需要可追溯性和高质量推理

11. 参考资料

  1. Wei, J., et al. “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models.” NeurIPS 2022.
  2. Yao, S., et al. “Tree of Thoughts: Deliberate Problem Solving with Large Language Models.” arXiv preprint arXiv:2305.10601, 2023.
  3. Spring AI Alibaba 官方文档: https://spring.io/projects/spring-ai-alibaba
  4. Server-Sent Events 规范: https://html.spec.whatwg.org/multipage/server-sent-events.html

12. 项目地址

欢迎 Star ⭐ 和贡献代码!


作者简介:一名热爱 AI 和全栈开发的程序员,专注于大模型应用开发和 RAG 系统构建。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐