构建聊天机器人

这一章我们用 Ollama 构建一个完整的聊天机器人,支持多轮对话、历史记录等功能。

基本聊天机器人

Python 版本

import ollama

def chatbot():
    print("聊天机器人已启动,输入 'exit' 退出")
    print("-" * 40)
    
    messages = [
        {'role': 'system', 'content': '你是一个友好的助手,回答简洁准确。'}
    ]
    
    while True:
        user_input = input("你: ")
        if user_input.lower() in ['exit', 'quit', '退出']:
            print("再见!")
            break
        
        messages.append({'role': 'user', 'content': user_input})
        
        response = ollama.chat(
            model='llama3.2',
            messages=messages
        )
        
        reply = response['message']['content']
        messages.append({'role': 'assistant', 'content': reply})
        
        print(f"助手: {reply}")
        print()

if __name__ == "__main__":
    chatbot()

流式输出版本

import ollama

def streaming_chatbot():
    print("聊天机器人已启动(流式输出)")
    print("-" * 40)
    
    messages = [
        {'role': 'system', 'content': '你是一个友好的助手。'}
    ]
    
    while True:
        user_input = input("你: ")
        if user_input.lower() in ['exit', 'quit']:
            break
        
        messages.append({'role': 'user', 'content': user_input})
        
        print("助手: ", end="", flush=True)
        
        full_reply = ""
        stream = ollama.chat(
            model='llama3.2',
            messages=messages,
            stream=True
        )
        
        for chunk in stream:
            text = chunk['message']['content']
            if text:
                print(text, end="", flush=True)
                full_reply += text
        
        print("\n")
        messages.append({'role': 'assistant', 'content': full_reply})

streaming_chatbot()

带历史记录的聊天机器人

import ollama
import json
from datetime import datetime

class ChatBot:
    def __init__(self, model='llama3.2', system=None, history_file=None):
        self.model = model
        self.messages = []
        self.history_file = history_file
        
        if system:
            self.messages.append({'role': 'system', 'content': system})
        
        if history_file:
            self.load_history()
    
    def load_history(self):
        try:
            with open(self.history_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
                self.messages = data.get('messages', self.messages)
        except FileNotFoundError:
            pass
    
    def save_history(self):
        if self.history_file:
            with open(self.history_file, 'w', encoding='utf-8') as f:
                json.dump({'messages': self.messages}, f, ensure_ascii=False, indent=2)
    
    def send(self, content):
        self.messages.append({'role': 'user', 'content': content})
        
        response = ollama.chat(
            model=self.model,
            messages=self.messages
        )
        
        reply = response['message']['content']
        self.messages.append({'role': 'assistant', 'content': reply})
        
        self.save_history()
        return reply
    
    def send_stream(self, content, callback=None):
        self.messages.append({'role': 'user', 'content': content})
        
        full_reply = ""
        stream = ollama.chat(
            model=self.model,
            messages=self.messages,
            stream=True
        )
        
        for chunk in stream:
            text = chunk['message']['content']
            if text:
                full_reply += text
                if callback:
                    callback(text)
        
        self.messages.append({'role': 'assistant', 'content': full_reply})
        self.save_history()
        
        return full_reply
    
    def clear(self):
        system_msg = next((m for m in self.messages if m['role'] == 'system'), None)
        self.messages = [system_msg] if system_msg else []
        self.save_history()
    
    def export_chat(self, filename):
        with open(filename, 'w', encoding='utf-8') as f:
            for msg in self.messages:
                role = {'system': '系统', 'user': '用户', 'assistant': '助手'}.get(msg['role'], msg['role'])
                f.write(f"[{role}]\n{msg['content']}\n\n")

def main():
    bot = ChatBot(
        model='llama3.2',
        system='你是一个友好的助手,回答简洁准确。',
        history_file='chat_history.json'
    )
    
    print("聊天机器人已启动(带历史记录)")
    print("命令: /clear 清空对话, /export 导出对话, /exit 退出")
    print("-" * 40)
    
    while True:
        user_input = input("你: ")
        
        if user_input.lower() in ['/exit', 'exit']:
            break
        elif user_input == '/clear':
            bot.clear()
            print("对话已清空\n")
            continue
        elif user_input == '/export':
            bot.export_chat(f'chat_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt')
            print("对话已导出\n")
            continue
        
        print("助手: ", end="", flush=True)
        bot.send_stream(user_input, lambda t: print(t, end="", flush=True))
        print("\n")

if __name__ == "__main__":
    main()

Web 聊天机器人

FastAPI 后端

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import ollama

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class ChatRequest(BaseModel):
    message: str
    history: list = []

@app.post("/chat")
async def chat(request: ChatRequest):
    messages = request.history + [
        {'role': 'user', 'content': request.message}
    ]
    
    response = ollama.chat(
        model='llama3.2',
        messages=messages
    )
    
    return {
        'reply': response['message']['content'],
        'history': messages + [{'role': 'assistant', 'content': response['message']['content']}]
    }

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    from fastapi.responses import StreamingResponse
    import json
    
    messages = request.history + [
        {'role': 'user', 'content': request.message}
    ]
    
    async def generate():
        stream = ollama.chat(
            model='llama3.2',
            messages=messages,
            stream=True
        )
        
        for chunk in stream:
            if chunk['message']['content']:
                yield f"data: {json.dumps({'content': chunk['message']['content']})}\n\n"
        
        yield "data: [DONE]\n\n"
    
    return StreamingResponse(generate(), media_type="text/event-stream")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

HTML 前端

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Ollama 聊天机器人</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; height: 100vh; display: flex; flex-direction: column; }
        .header { padding: 20px; background: #4a90d9; color: white; }
        .chat-container { flex: 1; overflow-y: auto; padding: 20px; }
        .message { margin-bottom: 15px; }
        .message.user { text-align: right; }
        .message .bubble { display: inline-block; padding: 10px 15px; border-radius: 15px; max-width: 70%; }
        .message.user .bubble { background: #4a90d9; color: white; }
        .message.assistant .bubble { background: white; }
        .input-container { padding: 20px; background: white; border-top: 1px solid #ddd; }
        .input-container form { display: flex; gap: 10px; }
        .input-container input { flex: 1; padding: 10px 15px; border: 1px solid #ddd; border-radius: 20px; outline: none; }
        .input-container button { padding: 10px 20px; background: #4a90d9; color: white; border: none; border-radius: 20px; cursor: pointer; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Ollama 聊天机器人</h1>
        </div>
        <div class="chat-container" id="chat"></div>
        <div class="input-container">
            <form id="form">
                <input type="text" id="input" placeholder="输入消息..." autocomplete="off">
                <button type="submit">发送</button>
            </form>
        </div>
    </div>
    
    <script>
        const chat = document.getElementById('chat');
        const form = document.getElementById('form');
        const input = document.getElementById('input');
        let history = [];
        
        function addMessage(role, content) {
            const div = document.createElement('div');
            div.className = `message ${role}`;
            div.innerHTML = `<div class="bubble">${content}</div>`;
            chat.appendChild(div);
            chat.scrollTop = chat.scrollHeight;
        }
        
        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            const message = input.value.trim();
            if (!message) return;
            
            addMessage('user', message);
            input.value = '';
            
            const response = await fetch('/chat/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message, history })
            });
            
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let reply = '';
            let assistantDiv = null;
            
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                
                const lines = decoder.decode(value).split('\n');
                for (const line of lines) {
                    if (line.startsWith('data: ') && line !== 'data: [DONE]') {
                        const data = JSON.parse(line.slice(6));
                        reply += data.content;
                        
                        if (!assistantDiv) {
                            assistantDiv = document.createElement('div');
                            assistantDiv.className = 'message assistant';
                            assistantDiv.innerHTML = '<div class="bubble"></div>';
                            chat.appendChild(assistantDiv);
                        }
                        
                        assistantDiv.querySelector('.bubble').textContent = reply;
                        chat.scrollTop = chat.scrollHeight;
                    }
                }
            }
            
            history.push(
                { role: 'user', content: message },
                { role: 'assistant', content: reply }
            );
        });
    </script>
</body>
</html>

命令行聊天机器人

import ollama
import argparse
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel

console = Console()

def rich_chatbot(model, system):
    console.print(Panel.fit("Ollama 聊天机器人", style="bold blue"))
    console.print("输入 /exit 退出, /clear 清空对话\n")
    
    messages = []
    if system:
        messages.append({'role': 'system', 'content': system})
    
    while True:
        try:
            user_input = console.input("[bold green]你:[/] ").strip()
        except KeyboardInterrupt:
            console.print("\n再见!")
            break
        
        if not user_input:
            continue
        
        if user_input == '/exit':
            console.print("再见!")
            break
        elif user_input == '/clear':
            messages = [m for m in messages if m['role'] == 'system']
            console.print("[yellow]对话已清空[/]\n")
            continue
        
        messages.append({'role': 'user', 'content': user_input})
        
        console.print("[bold blue]助手:[/]", end=" ")
        
        full_reply = ""
        stream = ollama.chat(model=model, messages=messages, stream=True)
        
        for chunk in stream:
            text = chunk['message']['content']
            if text:
                console.print(text, end="")
                full_reply += text
        
        console.print("\n")
        messages.append({'role': 'assistant', 'content': full_reply})

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--model', default='llama3.2')
    parser.add_argument('--system', default='你是一个友好的助手。')
    args = parser.parse_args()
    
    rich_chatbot(args.model, args.system)