Trae +ollama+qwen3-coder-30b-q4-k-m 本地部署

作为Trae 重度用户,每日需要频繁等待,配置 rtx-4090d -48G 显卡,可以尝试本地部署Qwen3-coder-30b-q4-k-m 原生 256K上下文;

ollama 部署本地模型,Trae 无法直接理解 大模型的工具调用,需要编写 Proxy-server,转发翻译ollama 的对话

## Ollama 优化 Trae 模型详细步骤
### 步骤1:准备基础模型

ollama pull qwen2.5-coder:32b


### 步骤2:创建 Modelfile
配置 256K 上下文窗口和优化参数

FROM qwen2.5-coder:32b TEMPLATE { { .Prompt }} SYSTEM '你是一个专业的代码助手,能够调用工具完成任务。' RENDERER qwen3-coder PARSER qwen3-coder PARAMETER repeat_penalty 1.05 PARAMETER stop <|im_start|> PARAMETER stop <|im_end|> PARAMETER stop <|endoftext|> PARAMETER temperature 0.5 PARAMETER top_k 20 PARAMETER top_p 0.9 PARAMETER num_ctx 262144

### 步骤3:构建自定义模型

ollama create local256K_trea:latest 
-f Modelfile

### 步骤4:开发 OpenAI 兼容 Proxy
- 清理系统提示词(移除参数详情、限制长度)
- 强制 JSON tool_calls 格式
- 转换 content 数组格式为纯文本
- 重映射 tool_call_id→name
- 双向转换 arguments 格式(字符串↔对象)
- 实现 XML→JSON 自动转换

server.js

const http = require('http'); const url = require('url'); const fs = require('fs'); const path = require('path'); const LOG_FILE = path.join(__dirname, 'proxy-debug.log'); const FORCE_MAX_TOKENS = 256000; const OLLAMA_HOST = 'localhost'; const OLLAMA_PORT = 11434; const PROXY_HOST = '0.0.0.0'; const PROXY_PORT = 8082; // ===== 日志工具 ===== const SEP = '='.repeat(80); function log(...args) { const ts = new Date().toISOString(); const line = `[${ts}] ${args.join(' ')}`; console.log(line); fs.appendFileSync(LOG_FILE, line + '\n', 'utf8'); } function logSection(title) { const sep = SEP; console.log(sep); console.log(title); console.log(sep); fs.appendFileSync(LOG_FILE, `${sep}\n${title}\n${sep}\n`, 'utf8'); } function logJson(label, obj) { const json = JSON.stringify(obj, null, 2); console.log(`[${label}]:`); console.log(json); fs.appendFileSync(LOG_FILE, `[${label}]:\n${json}\n`, 'utf8'); } // ===== Trae JSON tools → Qwen3 XML tool_call 格式转换 ===== // Trae 发送的是 OpenAI JSON tools 格式:{function:{name, description, parameters}} // Qwen3 需要 XML tool_call 格式:<tool_call><function=NAME>...<parameter=NAME>...</parameter></function></tool_call> function convertToolsToXml(tools) { if (!Array.isArray(tools) || tools.length === 0) return ''; // 只保留 name + description,不展开参数(参数太长会导致 prompt 膨胀到 48KB,压垮 32B Q4 模型) const toolBlocks = tools.map(tool => { const fn = tool.function || {}; const name = fn.name || 'unknown'; const desc = (fn.description || '').split('\n')[0].trim(); // 只取第一行 return `<tool_call><function=${name}>${desc}</function></tool_call>`; }); return '\n\n# Available tools\n' + toolBlocks.join('\n') + '\n\nIMPORTANT: When you need to use a tool, respond ONLY with the exact XML format above.'; } // ===== 清理 system prompt ===== function cleanSystemPrompt(content) { if (!content || typeof content !== 'string') return content; // 清理残留的 tool_call XML 标签(Trae 自己加的) return content .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '') .replace(/<function=\w+>[\s\S]*?<\/function>/g, '') .replace(/<parameter=\w+>[\s\S]*?<\/parameter>/g, ''); } // 修复双倍字符:Ollama Q4_K_M GGUF bug 导致 token 重复输出 // 跳过连续 ≥2 次重复的字符("PowerShell PowerShell" → "PowerShell", "Toolss" → "Tools") function fixDoubleChars(text) { if (!text || text.length < 2) return text; let result = ''; for (let i = 0; i < text.length; i++) { if (i > 0 && text[i] === text[i - 1]) continue; // 跳过连续重复 result += text[i]; } return result; } // 清理 tool result 中的 XML 标签,防止 Q4_K_M 模型在生成时崩溃(双倍字符 bug) function cleanToolResult(content) { if (!content || typeof content !== 'string') return content; // 提取纯文本,移除 Trae 的 XML 结构标签 return content .replace(/<toolcall_status>[\s\S]*?<\/toolcall_status>/gi, '') .replace(/<toolcall_result>/gi, '') .replace(/<\/toolcall_result>/gi, '') .replace(/<process_id>\d+<\/process_id>/gi, '') .replace(/<terminal_id>\d+<\/terminal_id>/gi, '') .replace(/<terminal_cwd>[^<]+<\/terminal_cwd>/gi, '') .replace(/<\/[^>]+>/g, '') // 移除剩余 XML 闭合标签 .replace(/<[^>]+>/g, '') // 移除剩余 XML 开放标签 .replace(/\s+/g, ' ') // 合并空白 .trim(); } function convertContent(msg) { if (!msg.content) { msg.content = ''; return; } if (Array.isArray(msg.content)) { msg.content = msg.content.map(c => c.text || c.content || '').join('\n'); } } // ===== XML → JSON tool_call 解析 ===== function parseXmlToolCall(xmlText) { const functionMatch = xmlText.match(/<function=(\w+)>/); if (!functionMatch) return null; const functionName = functionMatch[1]; const params = {}; const paramMatches = xmlText.match(/<parameter=(\w+)>\s*([\s\S]*?)\s*<\/parameter>/g); if (paramMatches) { for (const pm of paramMatches) { const match = pm.match(/&l