Python LangChain + Flask + Ollama deepseek-r1 实现一个简易AI问答系统

内容分享7天前发布
0 0 0

LangChain-Ollama 问答系统

最终效果

Python LangChain + Flask + Ollama deepseek-r1 实现一个简易AI问答系统

本篇博文将带领大家用python实现一个基于 LangChain 和 Ollama 的本地问答系统,使用
deepseek-r1:1.5b
模型提供智能问答功能。

功能特点

基于 LangChain 框架实现调用本地部署的 Ollama qwen3:8b 模型提供 Web 前端问答界面支持流式响应,实时显示回答内容简洁美观的用户界面

环境要求

Python 3.8+Ollama (已安装并运行 qwen3:8b 模型)pip (Python 包管理器)

完整代码

主程序 app.py


from flask import Flask, request, jsonify, stream_with_context, Response, send_from_directory
from flask_cors import CORS
from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
import threading
import queue
import json
import logging
import os

# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__, static_folder='.')
CORS(app, resources={r"/ask": {"origins": "*"}})  # 允许跨域请求

# 初始化 Ollama 模型
llm = None
try:
    llm = Ollama(model="deepseek-r1:1.5b", base_url="http://10.30.0.1:11434")
    logger.info("Ollama 模型初始化成功")
except Exception as e:
    logger.error(f"Ollama 模型初始化失败: {e}")

# 创建提示模板
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个 helpful 的 AI 助手。请用中文回答用户的问题。"),
    ("user", "{question}")
])

# 创建处理链
chain = None
if llm:
    chain = prompt_template | llm | StrOutputParser()

# 存储每个会话的结果队列
result_queues = {}

@app.route('/ask', methods=['POST'])
def ask_question():
    """处理问答请求"""
    data = request.json
    question = data.get('question') if data else None
    session_id = data.get('session_id', 'default') if data else 'default'
    
    if not question:
        return jsonify({"error": "问题不能为空"}), 400
    
    # 检查模型是否初始化成功
    if chain is None:
        return jsonify({"error": "模型未初始化,请检查 Ollama 服务是否运行正常"}), 500
    
    def generate():
        try:
            # 创建结果队列
            result_queue = queue.Queue()
            result_queues[session_id] = result_queue
            
            # 使用线程执行模型推理
            def run_model():
                try:
                    # 检查 chain 是否已初始化
                    if chain is None:
                        result_queue.put(("error", "模型未初始化,请检查 Ollama 服务是否运行正常"))
                        return
                    
                    # 使用流式输出
                    for chunk in chain.stream({"question": question}):
                        result_queue.put(("data", chunk))
                    result_queue.put(("end", ""))
                except Exception as e:
                    result_queue.put(("error", str(e)))
            
            thread = threading.Thread(target=run_model)
            thread.start()
            
            # 流式返回结果
            while True:
                try:
                    msg_type, content = result_queue.get(timeout=60)  # 60秒超时
                    if msg_type == "data":
                        yield f"data: {json.dumps({'chunk': content})}

"
                    elif msg_type == "end":
                        yield f"data: {json.dumps({'done': True})}

"
                        break
                    elif msg_type == "error":
                        yield f"data: {json.dumps({'error': content})}

"
                        break
                except queue.Empty:
                    yield f"data: {json.dumps({'error': '请求超时'})}

"
                    break
            
            # 清理会话队列
            if session_id in result_queues:
                del result_queues[session_id]
                
        except Exception as e:
            yield f"data: {json.dumps({'error': str(e)})}

"
    
    return Response(stream_with_context(generate()), mimetype='text/plain')

@app.route('/')
def index():
    """提供前端页面"""
    return send_from_directory('.', 'index.html')

@app.route('/health', methods=['GET'])
def health_check():
    """健康检查接口"""
    return jsonify({"status": "healthy"})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

前端页面 index.html


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LangChain-Ollama 问答系统</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            border-radius: 10px;
            padding: 30px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-area {
            display: flex;
            margin-bottom: 20px;
        }
        #question {
            flex: 1;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 4px 0 0 4px;
            font-size: 16px;
            outline: none;
        }
        #question:focus {
            border-color: #4CAF50;
        }
        #submit-btn {
            padding: 12px 24px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 0 4px 4px 0;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        #submit-btn:hover {
            background-color: #45a049;
        }
        #submit-btn:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        .chat-history {
            height: 400px;
            overflow-y: auto;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 15px;
            margin-bottom: 20px;
            background-color: #fafafa;
        }
        .message {
            margin-bottom: 15px;
            padding: 10px;
            border-radius: 4px;
        }
        .user-message {
            background-color: #e3f2fd;
            border-left: 4px solid #2196F3;
        }
        .ai-message {
            background-color: #f1f8e9;
            border-left: 4px solid #4CAF50;
        }
        .message-header {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .user-message .message-header {
            color: #2196F3;
        }
        .ai-message .message-header {
            color: #4CAF50;
        }
        .typing-indicator {
            color: #999;
            font-style: italic;
        }
        .error-message {
            color: #f44336;
            background-color: #ffebee;
            border-left: 4px solid #f44336;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>LangChain-Ollama 问答系统</h1>
        <div class="input-area">
            <input type="text" id="question" placeholder="请输入您的问题..." autocomplete="off">
            <button id="submit-btn">发送</button>
        </div>
        <div class="chat-history" id="chat-history"></div>
    </div>

    <script>
        // DOM 元素
        const questionInput = document.getElementById('question');
        const submitBtn = document.getElementById('submit-btn');
        const chatHistory = document.getElementById('chat-history');

        // 会话ID(简单实现,实际应用中可能需要更复杂的会话管理)
        const sessionId = 'session_' + Date.now();

        // 添加消息到聊天历史
        function addMessage(role, content) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${role}-message`;
            
            const header = document.createElement('div');
            header.className = 'message-header';
            header.textContent = role === 'user' ? '您:' : 'AI助手:';
            
            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            contentDiv.textContent = content;
            
            messageDiv.appendChild(header);
            messageDiv.appendChild(contentDiv);
            chatHistory.appendChild(messageDiv);
            
            // 滚动到底部
            chatHistory.scrollTop = chatHistory.scrollHeight;
            
            return messageDiv;
        }

        // 显示打字指示器
        function showTypingIndicator() {
            // 先移除任何现有的打字指示器
            removeTypingIndicator();
            
            const indicator = document.createElement('div');
            indicator.className = 'message ai-message typing-indicator';
            indicator.id = 'typing-indicator';
            indicator.innerHTML = '<div class="message-header">AI助手:</div><div class="message-content">正在思考中...</div>';
            chatHistory.appendChild(indicator);
            chatHistory.scrollTop = chatHistory.scrollHeight;
            return indicator;
        }

        // 移除打字指示器
        function removeTypingIndicator() {
            const indicator = document.getElementById('typing-indicator');
            if (indicator) {
                indicator.remove();
            }
        }

        // 发送问题到后端(使用 Fetch API 替代 EventSource)
        async function sendQuestion(question) {
            // 禁用按钮和输入框
            submitBtn.disabled = true;
            questionInput.disabled = true;
            
            try {
                // 添加用户问题到聊天历史
                addMessage('user', question);
                
                // 显示打字指示器
                showTypingIndicator();
                
                // 使用 Fetch API 发送请求
                const response = await fetch('/ask', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        question: question,
                        session_id: sessionId
                    })
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                // 处理流式响应
                const reader = response.body.getReader();
                const decoder = new TextDecoder('utf-8');
                let aiResponse = '';
                let aiMessageElement = null;
                
                while (true) {
                    const { done, value } = await reader.read();
                    
                    if (done) {
                        // 移除打字指示器
                        removeTypingIndicator();
                        // 完成响应
                        submitBtn.disabled = false;
                        questionInput.disabled = false;
                        questionInput.value = '';
                        break;
                    }
                    
                    // 解码接收到的数据
                    const chunk = decoder.decode(value, { stream: true });
                    const lines = chunk.split('

');
                    
                    for (const line of lines) {
                        if (line.startsWith('data: ')) {
                            try {
                                const dataStr = line.substring(6);
                                const data = JSON.parse(dataStr);
                                
                                if (data.done) {
                                    // 移除打字指示器
                                    removeTypingIndicator();
                                    // 完成响应
                                    submitBtn.disabled = false;
                                    questionInput.disabled = false;
                                    questionInput.value = '';
                                } else if (data.error) {
                                    // 移除打字指示器
                                    removeTypingIndicator();
                                    // 错误处理
                                    addMessage('error', `错误: ${data.error}`);
                                    submitBtn.disabled = false;
                                    questionInput.disabled = false;
                                } else if (data.chunk) {
                                    // 移除打字指示器(如果存在)
                                    removeTypingIndicator();
                                    
                                    // 接收响应块
                                    aiResponse += data.chunk;
                                    
                                    // 创建或更新AI消息
                                    if (!aiMessageElement) {
                                        aiMessageElement = addMessage('ai', aiResponse);
                                    } else {
                                        // 更新现有消息的内容
                                        const contentDiv = aiMessageElement.querySelector('.message-content');
                                        if (contentDiv) {
                                            contentDiv.textContent = aiResponse;
                                        }
                                    }
                                    
                                    // 滚动到底部
                                    chatHistory.scrollTop = chatHistory.scrollHeight;
                                }
                            } catch (e) {
                                console.error('Error parsing JSON:', e);
                            }
                        }
                    }
                }
                
            } catch (error) {
                console.error('Error sending question:', error);
                removeTypingIndicator();
                addMessage('error', '发送问题失败,请稍后重试。错误详情: ' + error.message);
                submitBtn.disabled = false;
                questionInput.disabled = false;
            }
        }

        // 事件监听器
        submitBtn.addEventListener('click', () => {
            const question = questionInput.value.trim();
            if (question) {
                sendQuestion(question);
            }
        });

        questionInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                const question = questionInput.value.trim();
                if (question) {
                    sendQuestion(question);
                }
            }
        });

        // 页面加载完成后聚焦输入框
        window.addEventListener('load', () => {
            questionInput.focus();
        });
    </script>
</body>
</html>

依赖 requirements.txt


langchain
langchain-community
flask
flask-cors
requests

安装步骤

克隆或下载本项目到本地

安装 Python 依赖包:


pip install -r requirements.txt

安装并配置 Ollama:

访问 Ollama 官网 下载并安装拉取 deepseek-r1:1.5b 模型:


ollama pull deepseek-r1:1.5b

启动 Ollama 服务(通常安装后会自动启动)验证模型是否正常工作:


ollama run deepseek-r1:1.5b "你好,介绍一下你自己"

启动后端服务:


python app.py

打开浏览器访问前端页面:
后端服务启动后,访问
http://localhost:5000
即可使用或者打开项目目录中的
index.html
文件

使用说明

在输入框中输入您的问题点击”发送”按钮或按回车键提交问题等待 AI 助手生成回答(回答会以流式方式逐步显示)可以继续提出更多问题进行对话

API 接口

问答接口

URL:
/ask
方法: POST参数:

question
(string): 用户提出的问题
session_id
(string, 可选): 会话标识符 响应: 流式文本数据

健康检查接口

URL:
/health
方法: GET响应: JSON 格式的健康状态信息

项目结构


.
├── app.py              # 后端 Flask 应用
├── index.html          # 前端页面
├── requirements.txt    # Python 依赖包列表
└── README.md          # 项目说明文档

技术栈

后端:Python, Flask, LangChain前端:HTML, CSS, JavaScriptAI 模型:Ollama deepseek-r1:1.5b

注意事项

确保 Ollama 服务正在运行首次运行可能需要下载模型,需要一定时间根据硬件配置不同,模型推理速度会有所差异如果遇到连接问题,请检查防火墙设置和端口占用情况

故障排除

如果提示找不到模型,请确认已执行
ollama pull deepseek-r1:1.5b
如果后端服务无法启动,请检查端口是否被占用(默认使用 5000 端口)如果前端页面无法连接后端,请确认后端服务已启动且网络连接正常如果模型响应很慢,可能是由于硬件资源限制,请耐心等待如果出现 “Connection refused” 或 “Max retries exceeded” 错误:
确保 Ollama 服务正在运行(在终端中执行
ollama serve
)检查 Ollama 是否在正确的端口上运行(默认是 11434)确认防火墙没有阻止连接尝试重启 Ollama 服务 如果访问主页出现 404 错误:
确保 Flask 应用已正确启动检查是否有端口冲突重新启动 Flask 应用 如果在浏览器中出现”发送问题失败,请稍后重试”错误:
检查浏览器控制台是否有错误信息确保 Flask 应用正在运行检查网络连接是否正常确认远程 Ollama 服务器可访问

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...