Skip to content

Commit fd11d17

Browse files
committed
fix:解决聊天过程中切换r1和非r1模型,以及中途中断插入话题等情况下的崩溃。增加聊天的健壮性
1 parent 63bae69 commit fd11d17

File tree

5 files changed

+92
-41
lines changed

5 files changed

+92
-41
lines changed

src/apiTools.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface Tool {
3232
}
3333

3434
export const toolRegistry: Map<string, Tool> = new Map();
35+
export const webSearchRegistry : Map<string, Tool> = new Map();
3536

3637
/**
3738
* 注册一个工具到全局工具表
@@ -41,6 +42,10 @@ export function registerTool(tool: Tool) {
4142
toolRegistry.set(tool.name, tool);
4243
}
4344

45+
export function registerWebSearchTool(tool: Tool) {
46+
webSearchRegistry.set(tool.name, tool);
47+
}
48+
4449
/**
4550
* Retrieves an array of all registered tools from the tool registry.
4651
* @returns An array of Tool objects.
@@ -49,6 +54,10 @@ export function getAllTools(): Tool[] {
4954
return Array.from(toolRegistry.values());
5055
}
5156

57+
export function getWebSearchTools(): Tool[] {
58+
return Array.from(webSearchRegistry.values());
59+
}
60+
5261
export async function handleNativeFunctionCalling(
5362
openai: OpenAI,
5463
modelName: string,
@@ -297,7 +306,7 @@ async function fetchPageContent(url: string): Promise<string> {
297306
}
298307

299308
// 1. 搜索网络
300-
export const searchTool: Tool = {
309+
export const webSearchTool: Tool = {
301310
name: 'web_search',
302311
description: `执行网络搜索并返回前5个结果的摘要。适用于需要获取外部信息、验证数据或了解最新动态的情况。
303312
用户提供这个工具通常是对你的知识储备或判断持怀疑态度,希望通过网络搜索获取更权威或更新的信息。
@@ -349,7 +358,8 @@ export const searchTool: Tool = {
349358
}
350359
},
351360
};
352-
registerTool(searchTool);
361+
registerTool(webSearchTool);
362+
registerWebSearchTool(webSearchTool);
353363

354364
// 获取网页正文内容的工具
355365
export const getWebpageContent: Tool = {
@@ -426,6 +436,7 @@ export const getWebpageContent: Tool = {
426436
};
427437

428438
registerTool(getWebpageContent);
439+
registerWebSearchTool(getWebpageContent);
429440

430441
// 列举指定路径下文件和目录的工具
431442
export const listDirectory: Tool = {

src/chatPanel.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ export class ChatPanel {
146146
<button id="stop" style="display:none;">Stop</button>
147147
<div class="web-search">
148148
<input type="checkbox" id="web-search">
149-
<label for="web-search">Agent模式(可联网)</label>
149+
<label for="web-search">联网搜索</label>
150+
</div>
151+
<div class="agent-mode">
152+
<input type="checkbox" id="agent-mode">
153+
<label for="agent-mode">Agent模式</label>
150154
</div>
151155
</div>
152156
</div>
@@ -179,6 +183,7 @@ export class ChatPanel {
179183
const stopButton = document.getElementById('stop');
180184
const newSessionButton = document.getElementById('new-session');
181185
const webSearchCheckbox = document.getElementById('web-search');
186+
const agentModeCheckbox = document.getElementById('agent-mode');
182187
const mermaidToggle = document.getElementById('mermaid-toggle');
183188
184189
// 加载外部脚本
@@ -272,11 +277,18 @@ export class ChatPanel {
272277
}
273278

274279
private async callModelApi(message: any, webviewOutputChannel: WebviewOutputChannel): Promise<string | null> {
275-
const tools = message.webSearch ? apiTools.getAllTools() : null;
280+
let tools = null;
276281
const normalSystemPrompt = "用markdown输出。如果有数学公式要用$$包裹,每条一行不要换行。如果有流程图(Mermaid)里的每个字符串都要用引号包裹。";
277-
const systemPrompt = message.webSearch
278-
? this.loadAgentPrompt() + normalSystemPrompt
279-
: normalSystemPrompt;
282+
let systemPrompt = normalSystemPrompt;
283+
284+
if (message.webSearch) {
285+
tools = apiTools.getWebSearchTools();
286+
systemPrompt += "每次回答问题前,先观察信息是否足够, 如果不够,先用tool_call进行网络搜索。不要盲目自信, 不要臆测不确定的信息。";
287+
}
288+
if (message.agentMode) {
289+
tools = apiTools.getAllTools();
290+
systemPrompt += this.loadAgentPrompt();
291+
}
280292

281293
return await callDeepSeekApi(
282294
this.conversation.map(msg => ({ role: msg.role, content: msg.content })),

src/deepseekApi.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ export async function processDeepSeekResponse(
142142
return { chunkResponse, finishReason };
143143
}
144144

145+
function fixMessages(messages: OpenAI.ChatCompletionMessageParam[]): OpenAI.ChatCompletionMessageParam[] {
146+
const result = [...messages]; // Create a copy to avoid mutating the original
147+
let i = 0;
148+
149+
// Traverse the array, but check length dynamically since we may insert elements
150+
while (i < result.length - 1) {
151+
const current = result[i];
152+
const next = result[i + 1];
153+
154+
// Check if current and next have the same role
155+
if (current.role === next.role) {
156+
// Insert the opposite role with content "ok"
157+
const newMessage : OpenAI.ChatCompletionMessageParam = {
158+
role: current.role === 'user' ? 'assistant' : 'user',
159+
content: 'ok'
160+
};
161+
// Insert at index i + 1
162+
result.splice(i + 1, 0, newMessage);
163+
// Move to the next pair, since we just inserted
164+
i += 2;
165+
} else {
166+
// Move to the next message if no insertion is needed
167+
i += 1;
168+
}
169+
}
170+
171+
return result;
172+
}
173+
145174
/**
146175
* 调用 DeepSeek API,支持 Function Calling
147176
* @param userContent 用户输入内容,可以是字符串或消息数组
@@ -194,39 +223,39 @@ export async function callDeepSeekApi(
194223
let maxToken = 1024 * 8;
195224
let temperature = 0;
196225

197-
let systemPromot : OpenAI.ChatCompletionMessageParam = {role : "system", content: systemContent};
198-
199-
if (/r1|reasoner/i.test(modelName)) {
226+
const isR1Mode = /r1|reasoner/i.test(modelName);
227+
if (isR1Mode) {
200228
temperature = 0.6;
201-
systemPromot = {role : "user", content: systemContent};
202229
}
203230

204-
// 构造消息体
231+
// 构造消息体,先将所有内容放入 messages_body
205232
let messages_body: OpenAI.ChatCompletionMessageParam[] = [];
206-
if (Array.isArray(userContent)) {
207-
if (systemPromot.role === 'user') {
208-
userContent[0].content = systemPromot.content + "\n\n" + userContent[0].content;
209-
}
210-
else{
211-
messages_body.push(systemPromot);
212-
}
213233

214-
const role = (userContent[0].role === 'user') ? 'user' : 'assistant';
215-
messages_body.push({ role, content: userContent[0].content });
234+
// 将 userContent 转换为数组并加入 messages_body
235+
const userContentArray = typeof userContent === 'string'
236+
? [{ role: 'user', content: userContent }]
237+
: userContent;
238+
for (const msg of userContentArray) {
239+
const role = msg.role === 'user' ? 'user' : 'assistant';
240+
messages_body.push({ role, content: msg.content });
241+
}
216242

217-
// 如果 userContent 是数组,按交替方式生成消息
218-
for (let i = 1; i < userContent.length; i++) {
219-
const role = (userContent[i].role === 'user') ? 'user' : 'assistant';
220-
messages_body.push({ role, content: userContent[i].content });
243+
if (messages_body.length > 0) {
244+
if (messages_body[0].role === 'user') {
245+
messages_body[0].content = (messages_body[0].content as string).replace(/<system>.*?<\/system>/gs, ''); // 替换 <system> 标签为空串
246+
} else {
247+
messages_body.shift(); // 移除头部系统消息
221248
}
222-
}
223-
else {
224-
if (systemPromot.role === 'user') { userContent = systemPromot.content + "\n\n" + userContent; }
225-
else{ messages_body.push(systemPromot); }
249+
}
226250

227-
// 如果是单个字符串,默认是 'user' 角色
228-
messages_body.push({ role: 'user', content: userContent });
251+
if (isR1Mode) {
252+
messages_body[0].content = `<system>${systemContent}</system>` + messages_body[0].content;
229253
}
254+
else {
255+
messages_body.unshift({role: 'system', content: `<system>${systemContent}</system>`});
256+
}
257+
258+
messages_body = fixMessages(messages_body);
230259

231260
vscode.window.showInformationMessage('开始上传DeepSeek API');
232261

src/resources/agentPrompt.txt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
每次回答问题前,不要太过自信, 先用tool收集一下信息,比如可以上网搜索或者查询记忆。
2-
第一步一定是先用读取记忆的tool检查以往的记忆里有没有相关信息。然后再看是否需要上网搜索。最后再看是否需要调用其他工具。一个tool没有返回就别急着推理下一步。
3-
每次只调用一个工具,然后结束输出等返回。然后再调用下一个工具。
4-
你要先理解任务,搞清楚哪些信息你是不知道的, 用记忆结构查询记忆,如果能查到就采用,
5-
然后指定任务计划,规划好分几个步骤,用tool去逐步去推进。
6-
等你觉得信息获得足够了再开始决策和输出结论。
7-
每次任务开始,你要先调取老记忆,看看以前有没有执行过类似任务,吸取经验。
8-
每次对话结束,如果获得了新的知识或者有感悟,或者我纠正了你某些内容,你可以用工具存储这些记忆。
1+
每次回答问题前,先观察信息是否足够, 如果不够,先用tool_call收集一下信息,优先级是: 调取记忆 > 本地文件读取 > 网络搜索。
2+
如果是比较复杂的需求,那要分步骤来。第一步一定是先用读取记忆的tool检查以往的记忆里有没有相关信息。然后再看是否需要上网搜索。最后再看是否需要调用其他tool_call。一个tool没有返回就别急着推理下一步。
3+
每次只调用一个tool_call,然后结束输出等返回。然后再调用下一个tool_call。
4+
你要先理解任务,搞清楚哪些信息你是不知道的, 用记忆tool_call查询过往记忆,查到的信息你自己判断是否和主题有关,无关的信息就无视掉,
5+
等你觉得信息足够了再开始决策和输出结论。
6+
你可以从老记忆中看看以前有没有执行过类似任务,吸取经验。
7+
每次对话结束,如果获得了新的知识或者有感悟,或者我纠正了你某些内容,你可以用tool_call存储这些记忆。

src/resources/chatPanelScript.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function separateThinkContent(input) {
208208
const parts = input.split(/(<think>[\s\S]*?<\/think>)/g);
209209

210210
parts.forEach(part => {
211-
if (!part) return;
211+
if (!part) {return; }
212212

213213
if (part.startsWith('<think>') && part.endsWith('</think>')) {
214214
segments.push({
@@ -345,7 +345,7 @@ function setupInputHandlers() {
345345
function sendMessage() {
346346
const text = input.value.trim();
347347
if (text) {
348-
vscode.postMessage({ command: 'sendMessage', text, webSearch: webSearchCheckbox.checked });
348+
vscode.postMessage({ command: 'sendMessage', text, webSearch: webSearchCheckbox.checked, agentMode: agentModeCheckbox.checked });
349349
input.value = '';
350350
}
351351
}

0 commit comments

Comments
 (0)