|
|
@@ -1,7 +1,6 @@
|
|
1
|
1
|
package com.ruoyi.web.llm.service.impl;
|
|
2
|
2
|
|
|
3
|
3
|
import com.alibaba.fastjson2.JSONObject;
|
|
4
|
|
-import com.ruoyi.common.config.RuoYiConfig;
|
|
5
|
4
|
import com.ruoyi.llm.domain.CmcChat;
|
|
6
|
5
|
import com.ruoyi.llm.service.ICmcChatService;
|
|
7
|
6
|
import com.ruoyi.web.llm.service.ILangChainMilvusService;
|
|
|
@@ -20,10 +19,6 @@ import org.springframework.beans.factory.annotation.Value;
|
|
20
|
19
|
import org.springframework.stereotype.Service;
|
|
21
|
20
|
import reactor.core.publisher.Flux;
|
|
22
|
21
|
|
|
23
|
|
-import java.io.BufferedReader;
|
|
24
|
|
-import java.io.File;
|
|
25
|
|
-import java.io.FileReader;
|
|
26
|
|
-import java.io.IOException;
|
|
27
|
22
|
import java.util.ArrayList;
|
|
28
|
23
|
import java.util.List;
|
|
29
|
24
|
|
|
|
@@ -42,7 +37,7 @@ public class SessionServiceImpl implements ISessionService {
|
|
42
|
37
|
@Override
|
|
43
|
38
|
public Flux<String> answer(String topicId, String question) {
|
|
44
|
39
|
McpClientProvider clientProvider = McpClientProvider.builder()
|
|
45
|
|
- .channel(McpChannel.STREAMABLE_STATELESS )
|
|
|
40
|
+ .channel(McpChannel.STREAMABLE_STATELESS)
|
|
46
|
41
|
.url("http://localhost:8087/mcp/sse")
|
|
47
|
42
|
.build();
|
|
48
|
43
|
ChatModel chatModel = ChatModel.of(llmServiceUrl)
|
|
|
@@ -59,122 +54,192 @@ public class SessionServiceImpl implements ISessionService {
|
|
59
|
54
|
messages.add(ChatMessage.ofAssistant(chat.getOutput()));
|
|
60
|
55
|
}
|
|
61
|
56
|
|
|
62
|
|
- // 读取SQL文件
|
|
63
|
|
- StringBuilder sqlContent = new StringBuilder();
|
|
64
|
|
- File sqlStructure = new File(RuoYiConfig.getProfile() + "/cmc_oa.sql");
|
|
65
|
|
- try (BufferedReader br = new BufferedReader(new FileReader(sqlStructure))) {
|
|
66
|
|
- String line;
|
|
67
|
|
- while ((line = br.readLine()) != null) {
|
|
68
|
|
- sqlContent.append(line).append("\n");
|
|
69
|
|
- }
|
|
70
|
|
- } catch (IOException e) {
|
|
71
|
|
- System.out.println("读取SQL文件失败");
|
|
72
|
|
- }
|
|
|
57
|
+ // 第一步:调用 GetAllTableNames 获取所有表名
|
|
|
58
|
+ String step1Prompt = "你是一个MySQL专家。请调用 GetAllTableNames 工具获取数据库中所有表名列表。";
|
|
|
59
|
+ messages.add(ChatMessage.ofUser(step1Prompt));
|
|
|
60
|
+ ChatSession session1 = InMemoryChatSession.builder().messages(messages).build();
|
|
|
61
|
+ Prompt prompt1 = Prompt.of(step1Prompt).attrPut("session", session1);
|
|
73
|
62
|
|
|
74
|
|
- // 优化prompt:明确要求不输出任何中间标记
|
|
75
|
|
- String prompt = "你是一个MySQL专家。根据以下OA系统表结构信息: \n" +
|
|
76
|
|
- sqlContent + " \n" +
|
|
77
|
|
- "用户查询: \n"+
|
|
78
|
|
- question + " \n"+
|
|
79
|
|
- "你可以使用SQLQuery工具来查询OA系统数据。 \n" +
|
|
80
|
|
- "SQLQuery工具功能:执行SQL查询语句,获取OA系统数据 \n" +
|
|
81
|
|
- "工具参数:sqlString(需要执行的SQL语句,格式为[SQL语句]) \n" +
|
|
82
|
|
- "要求: \n" +
|
|
83
|
|
- "1. 根据用户查询需求,决定是否需要查询数据 \n" +
|
|
84
|
|
- "2. 如果需要查询数据,调用SQLQuery工具并传入正确的SQL语句 \n" +
|
|
85
|
|
- "3. 生成标准MYSQL查询语句,根据语义和字段类型使用COUNT/SUM/AVG等聚合函数 \n" +
|
|
86
|
|
- "4. 如果单表字段不满足查询需求,根据OA系统表结构信息进行多张表连接查询 \n" +
|
|
87
|
|
- "5. 如果字段值由逗号隔开,根据OA系统表结构信息进行多张表连接查询,采用concat方式多值匹配 \n" +
|
|
88
|
|
- "6. 给生成的字段取一个简短的中文名称 \n" +
|
|
89
|
|
- "7. **重要:输出格式要求** \n" +
|
|
90
|
|
- " - 不要输出任何思考过程 \n" +
|
|
91
|
|
- " - 所有输出必须是完整的句子 \n" +
|
|
92
|
|
- " - 不要输出'查询结果'、'正在生成回答'等中间提示 \n" +
|
|
93
|
|
- " - 直接输出最终回答内容 \n" +
|
|
94
|
|
- " - 如果使用工具,工具调用和结果处理对用户透明,用户只看到最终答案 \n";
|
|
|
63
|
+ Flux<ChatResponse> chatResponse = chatModel.prompt(prompt1).stream();
|
|
|
64
|
+ Flux<String> contentFlux = chatResponse
|
|
|
65
|
+ .concatMap(resp -> {
|
|
|
66
|
+ try {
|
|
|
67
|
+ // 执行第一步:获取所有表名
|
|
|
68
|
+ String toolCall1 = "{\"name\": \"GetAllTableNames\", \"arguments\": {}}";
|
|
|
69
|
+ String toolResult1 = clientProvider.callTool("GetAllTableNames", new JSONObject()).getContent();
|
|
95
|
70
|
|
|
96
|
|
- messages.add(ChatMessage.ofUser(prompt));
|
|
97
|
|
- ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();
|
|
98
|
|
- Prompt wholePrompt = Prompt.of(prompt).attrPut("session", chatSession);
|
|
|
71
|
+ // 第二步:模型分析表名,找到相关表
|
|
|
72
|
+ List<ChatMessage> messages2 = new ArrayList<>(messages);
|
|
|
73
|
+ messages2.add(ChatMessage.ofAssistant("<tool_call>" + toolCall1 + "</tool_call>"));
|
|
|
74
|
+ messages2.add(ChatMessage.ofUser(toolResult1));
|
|
99
|
75
|
|
|
100
|
|
- Flux<ChatResponse> chatResponse = chatModel.prompt(wholePrompt).stream();
|
|
|
76
|
+ String step2Prompt = "请分析以下表名列表,找出与" + question + "相关的表。\n\n" +
|
|
|
77
|
+ "**输出格式要求**:\n" +
|
|
|
78
|
+ "- 如果需要数据库查询,请以固定格式输出:以\"相关表:\" 开头,中间用英文逗号隔开,以英文句号结尾\n" +
|
|
|
79
|
+ "- 如果不需要进行数据库查询,请直接回答。\n\n" + toolResult1;
|
|
|
80
|
+ messages2.add(ChatMessage.ofUser(step2Prompt));
|
|
|
81
|
+ ChatSession session2 = InMemoryChatSession.builder().messages(messages2).build();
|
|
101
|
82
|
|
|
102
|
|
- // 使用StringBuilder缓存完整响应,用于检测工具调用
|
|
103
|
|
- StringBuilder fullResponseBuilder = new StringBuilder();
|
|
104
|
|
- // 标记是否已经处理过工具调用
|
|
105
|
|
- boolean[] toolCallProcessed = {false};
|
|
106
|
|
- // 输出过滤状态:用于抑制 tool_call 区块(对用户不可见)
|
|
107
|
|
- StreamFilterState filterState = new StreamFilterState();
|
|
|
83
|
+ // 生成第二步的响应
|
|
|
84
|
+ return chatModel.prompt(Prompt.of(step2Prompt).attrPut("session", session2))
|
|
|
85
|
+ .stream()
|
|
|
86
|
+ .concatMap(resp1 -> {
|
|
|
87
|
+ try {
|
|
|
88
|
+ String content2 = resp1.getContent();
|
|
108
|
89
|
|
|
109
|
|
- Flux<String> contentFlux = chatResponse
|
|
110
|
|
- .concatMap(resp -> {
|
|
111
|
|
- String content = resp.getContent();
|
|
|
90
|
+ // 提取相关表名(从模型响应中提取)
|
|
|
91
|
+ String relevantTables = extractRelevantTables(content2);
|
|
112
|
92
|
|
|
113
|
|
- // 先累积原始内容,用于 tool_call 检测
|
|
114
|
|
- fullResponseBuilder.append(content);
|
|
115
|
|
- String currentFullContent = fullResponseBuilder.toString();
|
|
|
93
|
+ // 检查是否需要继续执行(如果包含工具调用或明确需要数据库查询)
|
|
|
94
|
+ if (relevantTables.equals("")) {
|
|
|
95
|
+ // 不需要数据库查询,直接返回最终回答
|
|
|
96
|
+ return Flux.just(content2)
|
|
|
97
|
+ .map(this::toSseDataFrame)
|
|
|
98
|
+ .concatWith(Flux.just("data: [DONE]\n\n"));
|
|
|
99
|
+ }
|
|
116
|
100
|
|
|
117
|
|
- // 1) 优先处理工具调用:检测到闭合的 <tool_call>...</tool_call> 后,静默调用工具并流式输出最终答案
|
|
118
|
|
- if (!toolCallProcessed[0]
|
|
119
|
|
- && currentFullContent.contains("<tool_call>")
|
|
120
|
|
- && currentFullContent.contains("</tool_call>")) {
|
|
121
|
|
- toolCallProcessed[0] = true;
|
|
122
|
|
- // 进入“最终回答阶段”前重置过滤状态
|
|
123
|
|
- filterState.reset();
|
|
124
|
|
- try {
|
|
125
|
|
- // 提取工具调用内容
|
|
126
|
|
- int start = currentFullContent.indexOf("<tool_call>");
|
|
127
|
|
- int end = currentFullContent.indexOf("</tool_call>");
|
|
128
|
|
- if (start < 0 || end < 0 || end <= start) {
|
|
129
|
|
- throw new IllegalStateException("未找到完整的 <tool_call>...</tool_call> 区块");
|
|
130
|
|
- }
|
|
131
|
|
- String toolContent = currentFullContent.substring(start + "<tool_call>".length(), end).trim();
|
|
132
|
|
- JSONObject jsonObject = JSONObject.parseObject(toolContent);
|
|
133
|
|
- String name = jsonObject.getString("name");
|
|
134
|
|
- JSONObject arguments = jsonObject.getJSONObject("arguments");
|
|
135
|
|
- String toolResult = "";
|
|
136
|
|
- try {
|
|
137
|
|
- // 调用工具
|
|
138
|
|
- toolResult = clientProvider.callTool(name, arguments).getContent();
|
|
139
|
|
- } catch (Exception e) {
|
|
140
|
|
- return Flux.just("抱歉,系统执行查询时遇到问题,请稍后重试。");
|
|
141
|
|
- } finally {
|
|
142
|
|
- clientProvider.close();
|
|
143
|
|
- }
|
|
|
101
|
+ // 第三步:调用 GetTableStructure 获取相关表的结构
|
|
|
102
|
+ List<ChatMessage> messages3 = new ArrayList<>(messages2);
|
|
|
103
|
+ messages3.add(ChatMessage.ofAssistant(content2));
|
|
144
|
104
|
|
|
145
|
|
- // 生成最终回答 - 不添加任何中间提示
|
|
146
|
|
- String finalPrompt = "基于以下查询结果,直接回答用户问题:\n" +
|
|
147
|
|
- "查询结果:\n" + toolResult + "\n\n" +
|
|
148
|
|
- "用户问题:\n" + question + "\n\n" +
|
|
149
|
|
- "要求:\n" +
|
|
150
|
|
- "1. 直接给出最终答案,不要输出任何中间过程\n" +
|
|
151
|
|
- "2. 不要输出'查询结果'、'根据查询结果'等前缀\n" +
|
|
152
|
|
- "3. 用完整的中文句子回答,语言自然流畅\n" +
|
|
153
|
|
- "4. 如果查询结果为空,如实说明";
|
|
|
105
|
+ // 执行第三步:获取表结构
|
|
|
106
|
+ StringBuilder tableStructures = new StringBuilder();
|
|
|
107
|
+ for (String tableName : relevantTables.split(",")) {
|
|
|
108
|
+ tableName = tableName.trim();
|
|
|
109
|
+ if (!tableName.isEmpty()) {
|
|
|
110
|
+ JSONObject args = new JSONObject();
|
|
|
111
|
+ args.put("tableName", tableName);
|
|
|
112
|
+ String toolResult3 = clientProvider.callTool("GetTableStructure", args)
|
|
|
113
|
+ .getContent();
|
|
|
114
|
+ tableStructures.append("表:").append(tableName).append("\n")
|
|
|
115
|
+ .append(toolResult3)
|
|
|
116
|
+ .append("\n\n");
|
|
|
117
|
+ }
|
|
|
118
|
+ }
|
|
154
|
119
|
|
|
155
|
|
- return chatModel.prompt(finalPrompt).stream()
|
|
156
|
|
- .map(ChatResponse::getContent)
|
|
157
|
|
- .filter(answer -> answer != null && !answer.isEmpty());
|
|
|
120
|
+ // 第四步:模型生成 SQL 查询
|
|
|
121
|
+ List<ChatMessage> messages4 = new ArrayList<>(messages3);
|
|
|
122
|
+ messages4.add(ChatMessage.ofAssistant(
|
|
|
123
|
+ "<tool_call>{\"name\": \"GetTableStructure\", \"arguments\": {\"tableName\": \"employee\"}}</tool_call>"));
|
|
|
124
|
+ messages4.add(ChatMessage.ofUser(tableStructures.toString()));
|
|
158
|
125
|
|
|
159
|
|
- } catch (Exception e) {
|
|
160
|
|
- return Flux.just("抱歉,系统执行查询时遇到问题,请稍后重试。");
|
|
161
|
|
- }
|
|
162
|
|
- }
|
|
|
126
|
+ String step4Prompt = "请根据以下表结构,生成" + question + "的SQL语句:\n\n"
|
|
|
127
|
+ + tableStructures.toString();
|
|
|
128
|
+ messages4.add(ChatMessage.ofUser(step4Prompt));
|
|
163
|
129
|
|
|
164
|
|
- // 2) 对用户可见输出:统一用“容错状态机”剥离<tool_call> 区块
|
|
165
|
|
- // 不再使用“未闭合 tool_call 就整段屏蔽”的策略,避免吞掉真正回答。
|
|
166
|
|
- String visible = filterVisibleChunk(content, filterState);
|
|
167
|
|
- return visible.isEmpty() ? Flux.empty() : Flux.just(visible);
|
|
168
|
|
- });
|
|
|
130
|
+ // 生成第四步的响应
|
|
|
131
|
+ return chatModel
|
|
|
132
|
+ .prompt(Prompt.of(step4Prompt).attrPut("session",
|
|
|
133
|
+ InMemoryChatSession.builder().messages(messages4).build()))
|
|
|
134
|
+ .stream()
|
|
|
135
|
+ .concatMap(resp2 -> {
|
|
|
136
|
+ try {
|
|
|
137
|
+ String content4 = resp2.getContent();
|
|
|
138
|
+
|
|
|
139
|
+ // 提取 SQL 语句
|
|
|
140
|
+ String sqlQuery = extractSqlFromContent(content4);
|
|
|
141
|
+
|
|
|
142
|
+ // 第五步:调用 SQLQuery 执行查询
|
|
|
143
|
+ List<ChatMessage> messages5 = new ArrayList<>(messages4);
|
|
|
144
|
+ messages5.add(ChatMessage.ofAssistant(content4));
|
|
|
145
|
+
|
|
|
146
|
+ // 执行第五步:执行 SQL 查询
|
|
|
147
|
+ JSONObject sqlArgs = new JSONObject();
|
|
|
148
|
+ sqlArgs.put("sqlString", sqlQuery);
|
|
|
149
|
+ String toolResult5 = clientProvider
|
|
|
150
|
+ .callTool("SQLQuery", sqlArgs)
|
|
|
151
|
+ .getContent();
|
|
|
152
|
+
|
|
|
153
|
+ // 第六步:模型生成最终回答
|
|
|
154
|
+ List<ChatMessage> messages6 = new ArrayList<>(messages5);
|
|
|
155
|
+ messages6.add(ChatMessage.ofAssistant(
|
|
|
156
|
+ "<tool_call>{\"name\": \"SQLQuery\", \"arguments\": {\"sqlString\": \""
|
|
|
157
|
+ + sqlQuery + "\"}}</tool_call>"));
|
|
|
158
|
+ messages6.add(ChatMessage.ofUser(toolResult5));
|
|
169
|
159
|
|
|
|
160
|
+ String step6Prompt = "请根据以下查询结果,生成关于" + question + "的最终回答:\n\n"
|
|
|
161
|
+ + toolResult5;
|
|
|
162
|
+ messages6.add(ChatMessage.ofUser(step6Prompt));
|
|
|
163
|
+
|
|
|
164
|
+ // 生成第六步的响应(流式)
|
|
|
165
|
+ return chatModel
|
|
|
166
|
+ .prompt(Prompt.of(step6Prompt).attrPut("session",
|
|
|
167
|
+ InMemoryChatSession.builder()
|
|
|
168
|
+ .messages(messages6)
|
|
|
169
|
+ .build()))
|
|
|
170
|
+ .stream()
|
|
|
171
|
+ .map(resp3 -> resp3.getContent())
|
|
|
172
|
+ .filter(answer -> answer != null && !answer.isEmpty());
|
|
|
173
|
+ } catch (Exception e) {
|
|
|
174
|
+ return Flux.just("抱歉,系统执行查询时遇到问题,请稍后重试。")
|
|
|
175
|
+ .map(this::toSseDataFrame)
|
|
|
176
|
+ .concatWith(Flux.just("data: [DONE]\n\n"));
|
|
|
177
|
+ }
|
|
|
178
|
+ });
|
|
|
179
|
+ } catch (Exception e) {
|
|
|
180
|
+ return Flux.just("抱歉,系统执行查询时遇到问题,请稍后重试。")
|
|
|
181
|
+ .map(this::toSseDataFrame)
|
|
|
182
|
+ .concatWith(Flux.just("data: [DONE]\n\n"));
|
|
|
183
|
+ }
|
|
|
184
|
+ });
|
|
|
185
|
+ } catch (Exception e) {
|
|
|
186
|
+ return Flux.just("抱歉,系统执行查询时遇到问题,请稍后重试。")
|
|
|
187
|
+ .map(this::toSseDataFrame)
|
|
|
188
|
+ .concatWith(Flux.just("data: [DONE]\n\n"));
|
|
|
189
|
+ }
|
|
|
190
|
+ });
|
|
170
|
191
|
// 手工拼接 SSE 帧,绕过 ServerSentEvent 编码差异;并在末尾发送 [DONE]
|
|
171
|
192
|
Flux<String> sseFlux = contentFlux
|
|
172
|
193
|
.map(this::toSseDataFrame)
|
|
173
|
194
|
.concatWith(Flux.just("data: [DONE]\n\n"));
|
|
174
|
|
-
|
|
175
|
195
|
return sseFlux;
|
|
176
|
196
|
}
|
|
177
|
197
|
|
|
|
198
|
+ /**
|
|
|
199
|
+ * 从模型响应中提取相关表名
|
|
|
200
|
+ */
|
|
|
201
|
+ private String extractRelevantTables(String content) {
|
|
|
202
|
+ // 简化处理,实际应该根据模型输出格式提取
|
|
|
203
|
+ // 假设模型会返回类似 "相关表:employee, department"
|
|
|
204
|
+ if (content.contains("相关表:")) {
|
|
|
205
|
+ int start = content.indexOf("相关表:") + 4;
|
|
|
206
|
+ int end = content.indexOf(".", start);
|
|
|
207
|
+ if (end > start) {
|
|
|
208
|
+ String tables = content.substring(start, end).trim();
|
|
|
209
|
+ // 限制相关表最多只保留10个
|
|
|
210
|
+ String[] tableNames = tables.split(",");
|
|
|
211
|
+ if (tableNames.length > 10) {
|
|
|
212
|
+ StringBuilder limitedTables = new StringBuilder();
|
|
|
213
|
+ for (int i = 0; i < 10; i++) {
|
|
|
214
|
+ if (i > 0) {
|
|
|
215
|
+ limitedTables.append(",");
|
|
|
216
|
+ }
|
|
|
217
|
+ limitedTables.append(tableNames[i].trim());
|
|
|
218
|
+ }
|
|
|
219
|
+ return limitedTables.toString();
|
|
|
220
|
+ }
|
|
|
221
|
+ return tables;
|
|
|
222
|
+ }
|
|
|
223
|
+ }
|
|
|
224
|
+ // 默认返回可能相关的表
|
|
|
225
|
+ return "";
|
|
|
226
|
+ }
|
|
|
227
|
+
|
|
|
228
|
+ /**
|
|
|
229
|
+ * 从模型响应中提取 SQL 语句
|
|
|
230
|
+ */
|
|
|
231
|
+ private String extractSqlFromContent(String content) {
|
|
|
232
|
+ // 简化处理,实际应该根据模型输出格式提取
|
|
|
233
|
+ if (content.contains("SELECT")) {
|
|
|
234
|
+ int start = content.indexOf("SELECT");
|
|
|
235
|
+ int end = content.indexOf(";", start);
|
|
|
236
|
+ if (end > start) {
|
|
|
237
|
+ return content.substring(start, end + 1);
|
|
|
238
|
+ }
|
|
|
239
|
+ }
|
|
|
240
|
+ return "";
|
|
|
241
|
+ }
|
|
|
242
|
+
|
|
178
|
243
|
@Override
|
|
179
|
244
|
public Flux<AssistantMessage> answerWithDocument(String topicId, String chatId, String question) throws Exception {
|
|
180
|
245
|
return langChainMilvusService.generateAnswerWithDocument(topicId, chatId, question);
|
|
|
@@ -204,66 +269,4 @@ public class SessionServiceImpl implements ISessionService {
|
|
204
|
269
|
return sb.toString();
|
|
205
|
270
|
}
|
|
206
|
271
|
|
|
207
|
|
- private static final class StreamFilterState {
|
|
208
|
|
- boolean inToolCall = false;
|
|
209
|
|
-
|
|
210
|
|
- void reset() {
|
|
211
|
|
- inToolCall = false;
|
|
212
|
|
- }
|
|
213
|
|
- }
|
|
214
|
|
-
|
|
215
|
|
- /**
|
|
216
|
|
- * 对用户可见输出过滤:
|
|
217
|
|
- * - 丢弃 <tool_call>...</tool_call> 内容(容错:缺失 </tool_call> 时,遇到空行分隔则自动退出 tool_call)
|
|
218
|
|
- * 说明:工具调用检测/执行仍基于原始累积文本,不依赖该过滤器。
|
|
219
|
|
- */
|
|
220
|
|
- private String filterVisibleChunk(String chunk, StreamFilterState st) {
|
|
221
|
|
- if (chunk == null || chunk.isEmpty()) {
|
|
222
|
|
- return "";
|
|
223
|
|
- }
|
|
224
|
|
- String s = chunk.replace("\r\n", "\n");
|
|
225
|
|
- StringBuilder out = new StringBuilder(s.length());
|
|
226
|
|
- int i = 0;
|
|
227
|
|
- while (i < s.length()) {
|
|
228
|
|
- // 在 tool_call 内:优先寻找 </tool_call>;找不到则容错以空行分隔退出
|
|
229
|
|
- if (st.inToolCall) {
|
|
230
|
|
- int end = s.indexOf("</tool_call>", i);
|
|
231
|
|
- if (end >= 0) {
|
|
232
|
|
- st.inToolCall = false;
|
|
233
|
|
- i = end + "</tool_call>".length();
|
|
234
|
|
- continue;
|
|
235
|
|
- }
|
|
236
|
|
- int sep = s.indexOf("\n\n", i);
|
|
237
|
|
- if (sep >= 0) {
|
|
238
|
|
- st.inToolCall = false;
|
|
239
|
|
- i = sep + 2;
|
|
240
|
|
- continue;
|
|
241
|
|
- }
|
|
242
|
|
- // 剩余都认为在 tool_call 内
|
|
243
|
|
- break;
|
|
244
|
|
- }
|
|
245
|
|
-
|
|
246
|
|
- // 不在任何区块内:找下一个起始标签
|
|
247
|
|
- int nextTool = s.indexOf("<tool_call>", i);
|
|
248
|
|
- int next;
|
|
249
|
|
- if (nextTool == -1) {
|
|
250
|
|
- out.append(s.substring(i));
|
|
251
|
|
- break;
|
|
252
|
|
- } else {
|
|
253
|
|
- next = nextTool;
|
|
254
|
|
- }
|
|
255
|
|
-
|
|
256
|
|
- if (next > i) {
|
|
257
|
|
- out.append(s, i, next);
|
|
258
|
|
- }
|
|
259
|
|
- st.inToolCall = true;
|
|
260
|
|
- i = next + "<tool_call>".length();
|
|
261
|
|
- }
|
|
262
|
|
-
|
|
263
|
|
- // 兜底移除残留标签字面量(避免显示到前端)
|
|
264
|
|
- return out.toString()
|
|
265
|
|
- .replace("<tool_call>", "")
|
|
266
|
|
- .replace("</tool_call>", "");
|
|
267
|
|
- }
|
|
268
|
|
-
|
|
269
|
272
|
}
|