时隔一年没在社区写文章,这里随缘更新下。
最近两年都在做AI相关的东西,玩得不亦乐乎。
本地玩大模型,Ollama算是一个比较方便的方式,只需在Ollama官网下载Ollama客户端,安装,启动,然后就可以通过ollama命令安装、使用大部分市面上的热门模型。
Ollama官网地址:
https://ollama.comOllama可用模型列表:
https://ollama.com/searchOllama拉取模型,以最新qwen3:0.6b为例,需命令行执行:
$ ollama pull qwen3:0.6b或者也可以直接运行模型,运行时如果模型还没有被下载,这时候会自动拉取:
$ ollama run qwen3:0.6b当模型运行后,此时就可以通过命令行跟这个模型对话了。
这里让AI写了个简易客户端,简单做了些调整,直接把这段代码保存到本地,作为html文件,使用浏览器打开,就可以连接本地的Ollama里的模型作对话。
需要注意的是,模型要先预先下载好。
客户端代码如下:
<!DOCTYPE html><html lang="zh"><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Chat Bot</title> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> <script src="https://cdn.uv.cc/markedjs/0.3.5/marked.min.js"></script> <style> body { margin: 0; font-family: Arial, sans-serif; background-color: #f4f4f4; height: 100vh; display: flex; flex-direction: column; } header { background-color: #333; color: white; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; position: fixed; width: 100%; } header button { background-color: #555; border: none; color: white; padding: 5px 10px; cursor: pointer; font-size: 14px; border-radius: 4px; margin-right: 35px; } #settings-panel { background-color: #fff; border-bottom: 1px solid #ccc; margin-top: 45px; position: fixed; width: 100vw; padding: 15px 60px 15px 15px; } .settings-row { margin-bottom: 10px; } .settings-row label { display: inline-block; width: 100px; } .settings-row input { width: calc(100% - 150px); padding: 5px; box-sizing: border-box; } #chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; margin-top: 40px; margin-bottom: 50px; } .message { max-width: 100%; padding: 10px 10px 0; border-radius: 10px; word-wrap: break-word; margin-bottom: 15px; align-self: flex-end; } .user { background-color: #007bff; color: white; float: right; max-width: 85%; padding: 10px; border-radius: 10px; word-wrap: break-word; margin-bottom: 15px; align-self: flex-end; } .bot-wrapper { background-color: white; max-width: 85%; padding: 10px; border-radius: 10px; word-wrap: break-word; margin-bottom: 15px; align-self: flex-end; } .bot-answer { } .bot-thoughts { cursor: pointer; font-weight: bold; padding: 8px; border-radius: 5px; max-width: 70%; align-self: flex-end; } .bot-thoughts.collapsed::after { content: " ▼"; } .bot-thoughts.expanded::after { content: " ▲"; } .bot-thoughts-content { padding: 0 10px 10px; max-width: 100%; align-self: flex-end; color: #999; font-size: 14px; } form { position: fixed; bottom: 0; left: 0; right: 0; background-color: #fff; padding: 10px; border-top: 1px solid #ccc; display: flex; } input[type="text"] { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px; } .send-input { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px; } .send-btn { margin-left: 10px; padding: 10px 20px; font-size: 16px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; } .stop-btn { margin-left: 10px; padding: 10px 20px; font-size: 16px; background-color: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer; } .clear-btn { background-color: #f44336; color: white; border: none; padding: 5px 10px; cursor: pointer; font-size: 14px; border-radius: 4px; } .reset-btn { margin-left: 10px; background-color: white; color: black; padding: 5px 10px; cursor: pointer; font-size: 14px; border-radius: 4px; border: 1px solid #007bff; } </style></head><body> <div id="app"> <header> <div>Chat Bot</div> <button @click="toggleSettings">{{showSettings ? '收起面板' : '设置'}}</button> </header> <div id="settings-panel" v-show="showSettings"> <div class="settings-row"> <label for="systemPrompt">系统提示词</label> <input type="text" id="systemPrompt" @change="saveSettings" v-model="settings.systemPrompt" /> </div> <div class="settings-row"> <label for="maxHistory">记忆轮数</label> <input type="number" id="maxHistory" @change="saveSettings" v-model.number="settings.maxHistory" min="1" max="20" /> </div> <div class="settings-row"> <label for="temperature">温度</label> <input type="number" id="temperature" @change="saveSettings" v-model.number="settings.temperature" step="0.1" min="0" max="1" /> </div> <div class="settings-row"> <label for="model">模型</label> <input type="text" id="model" @change="saveSettings" v-model.number="settings.model" step="0.1" min="0" max="1" /> </div> <div class="settings-row"> <label for="model">服务地址</label> <input type="text" id="addr" @change="saveSettings" v-model.number="settings.addr" step="0.1" min="0" max="1" /> </div> <div class="settings-row"> <button class="clear-btn" @click="clearHistory">清空对话</button> <button class="reset-btn" @click="resetSetting">恢复默认</button> </div> </div> <div id="chat-container" ref="chatContainer"> <div v-for="(msg, index) in messages" :key="index"> <div v-if="msg.role === 'user'" class="user">{{ msg.content }}</div> <div v-else-if="msg.thoughts !== undefined && msg.answer !== undefined" class="bot-wrapper"> <div v-show="msg.thoughts" class="bot-thoughts" :class="msg.collapsed ? 'collapsed' : 'expanded'" @click="toggleThoughts(msg)" :ref="(el) => setRef(el, index, 'thoughtHeader')"> 思考内容 </div> <div v-show="msg.thoughts && msg.collapsed" class="bot-thoughts-content" :ref="(el) => setRef(el, index, 'thoughtContent')" v-html="compiledMarkdown(msg.thoughts)"></div> <hr v-show="msg.thoughts && msg.answer"/> <div v-show="msg.answer" class="message bot-answer" v-html="compiledMarkdown(msg.answer)"></div> </div> <div v-else-if="msg.role === 'assistant' && msg.content !== undefined" class="message bot-answer"> {{ msg.content }} </div> </div> </div> <form @submit.prevent="sendMessage"> <input type="text" class="send-input" v-model="inputMessage" placeholder="输入消息..." required /> <input type="submit" v-show="!sending" class="send-btn" value="发送" /> <input type="button" v-show="sending" class="stop-btn" value="停止" @click="stopMessage" /> </form> </div> <script> const { createApp, ref, onMounted } = Vue; createApp({ setup() { // 输入框消息 const inputMessage = ref(''); // 是否在发送中标识 const sending = ref(false) // 加载本地缓存消息 const localMessages = JSON.parse(localStorage.getItem('chatMessages')) || [] localMessages.map(item => { if (item.role === 'assistant') { // 默认历史消息折叠思考内容 item.collapsed = false } }) const messages = ref(localMessages); // 是否展示设置面板 const showSettings = ref(false); const chatContainer = ref(null); // 加载已缓存设置或者默认配置 const defaultSettings = { systemPrompt: '你是一个助手。', maxHistory: 5, temperature: 0.7, model: 'qwen3:0.6b', addr: 'http://localhost:11434' }; const settings = ref(JSON.parse(localStorage.getItem('chatSettings')) || defaultSettings); // 创建一个AbortController实例,用来控制对话中断 let controller = null; const refs = ref({}); // 当前正在接收的 assistant 消息索引 const currentBotMsgIndex = ref(null); function setRef(el, index, key) { if (!refs.value[index]) refs.value[index] = {}; refs.value[index][key] = el; } function toggleThoughts(msg) { msg.collapsed = !msg.collapsed saveMessages() } function saveMessages() { localStorage.setItem('chatMessages', JSON.stringify(messages.value)); } function saveSettings() { localStorage.setItem('chatSettings', JSON.stringify(settings.value)); } function resetSetting () { if (confirm("确定要恢复默认设置吗?")) { settings.value = defaultSettings saveSettings() } } function clearHistory() { if (confirm("确定要清空对话历史吗?")) { stopMessage() messages.value = messages.value.filter(msg => msg.role === 'system'); saveMessages(); } } function renderMessages() { setTimeout(() => { chatContainer.value.scrollTop = chatContainer.value.scrollHeight; }, 0); } function toggleSettings() { showSettings.value = !showSettings.value; } async function sendMessage() { const message = inputMessage.value.trim(); if (!message) return; // 标识对话进行中 sending.value = true; const userMsg = { role: 'user', content: message }; messages.value.push(userMsg); inputMessage.value = ''; // saveMessages(); renderMessages(); const history = [...messages.value] .filter(m => m.role === 'user' || (m.role === 'assistant' && m.answer)) .map(m => m.role === 'assistant' ? { role: m.role, content: m.answer } : m) .slice(-settings.value.maxHistory); const payload = { model: settings.value.model, stream: true, temperature: settings.value.temperature, messages: [ { role: 'system', content: settings.value.systemPrompt }, ...history ] }; try { // 创建一个新的AbortController controller = new AbortController(); // 获取信号 let signal = controller.signal; const response = await fetch(settings.value.addr + '/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal }); // debugger // if (!response.ok || !response.body) throw new Error('请求失败或不支持流式响应'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullResponse = ''; const botMsg = { role: 'assistant', thoughts: '', answer: '', collapsed: true }; messages.value.push(botMsg); currentBotMsgIndex.value = messages.value.length - 1; // saveMessages(); renderMessages(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); if (!response.ok) { alert(chunk) return } const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.slice(6).trim(); if (dataStr === '[DONE]') continue; try { const data = JSON.parse(dataStr); const content = data.choices?.[0]?.delta?.content; if (!content) continue; fullResponse += content; console.log(content) if (fullResponse.startsWith('<think>') && fullResponse.indexOf("</think>") < 0) { messages.value[currentBotMsgIndex.value].thoughts = fullResponse.replace("<think>\n", '') } else { // 尝试解析 think / answer if (fullResponse.startsWith('<think>')) { messages.value[currentBotMsgIndex.value].answer = fullResponse.split('</think>\n\n')[1] } else { messages.value[currentBotMsgIndex.value].answer = fullResponse } } saveMessages(); renderMessages(); } catch (error) { if (error.name === 'AbortError') { console.log('Fetch aborted'); } else { console.error('Fetch error:', error); } } } } } // 如果最终没有进入 think/answer 结构,则作为普通回答处理 const bot = messages.value[currentBotMsgIndex.value]; if (bot.content) { bot.answer = bot.content; delete bot.content; } saveMessages(); renderMessages(); } catch (error) { console.error('请求失败:', error); // alert(error.message) } finally { controller = null; sending.value = false; } } function stopMessage () { if (controller) { controller.abort(); } } function compiledMarkdown (text) { return marked(text); } onMounted(() => { renderMessages(); }); return { inputMessage, sending, messages, showSettings, settings, setRef, toggleThoughts, clearHistory, sendMessage, stopMessage, toggleSettings, chatContainer, renderMessages, saveSettings, resetSetting, compiledMarkdown }; } }).mount('#app'); </script></body></html>以上,Enjoy!