LangChain-Ollama 问答系统
最终效果

本篇博文将带领大家用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: 方法: POST参数:
/ask
(string): 用户提出的问题
question (string, 可选): 会话标识符 响应: 流式文本数据
session_id
健康检查接口
URL: 方法: GET响应: JSON 格式的健康状态信息
/health
项目结构
.
├── app.py # 后端 Flask 应用
├── index.html # 前端页面
├── requirements.txt # Python 依赖包列表
└── README.md # 项目说明文档
技术栈
后端:Python, Flask, LangChain前端:HTML, CSS, JavaScriptAI 模型:Ollama deepseek-r1:1.5b
注意事项
确保 Ollama 服务正在运行首次运行可能需要下载模型,需要一定时间根据硬件配置不同,模型推理速度会有所差异如果遇到连接问题,请检查防火墙设置和端口占用情况
故障排除
如果提示找不到模型,请确认已执行 如果后端服务无法启动,请检查端口是否被占用(默认使用 5000 端口)如果前端页面无法连接后端,请确认后端服务已启动且网络连接正常如果模型响应很慢,可能是由于硬件资源限制,请耐心等待如果出现 “Connection refused” 或 “Max retries exceeded” 错误:
ollama pull deepseek-r1:1.5b
确保 Ollama 服务正在运行(在终端中执行 )检查 Ollama 是否在正确的端口上运行(默认是 11434)确认防火墙没有阻止连接尝试重启 Ollama 服务 如果访问主页出现 404 错误:
ollama serve
确保 Flask 应用已正确启动检查是否有端口冲突重新启动 Flask 应用 如果在浏览器中出现”发送问题失败,请稍后重试”错误:
检查浏览器控制台是否有错误信息确保 Flask 应用正在运行检查网络连接是否正常确认远程 Ollama 服务器可访问


