Odoo 18 实现企业级 AI Chat:WebSocket 架构设计与性能优化
🏷️ 标签: WebSocket AI聊天 Tornado 实时通信 Odoo集成 流式响应
📅 发布: 2025-08-16 | 📝 作者: melon | ⏱️ 阅读: ~25分钟
📖 概述
在当今AI技术飞速发展的时代,实时聊天系统已成为许多应用的核心功能。本文深入探讨如何使用WebSocket技术构建一个功能完整、性能卓越的实时AI聊天系统。
🎯 系统亮点
- 实时性: 基于WebSocket的双向通信,支持流式AI响应
- 企业级: 集成Odoo用户系统,提供完整的权限管理
- 用户体验: 现代化UI设计,支持Markdown渲染和代码高亮
- 可扩展: 模块化架构,易于功能扩展和维护
🔧 技术特色
该系统采用前后端分离架构,后端使用Tornado框架实现高性能WebSocket服务,前端通过现代Web技术实现流式聊天界面,并深度集成Odoo系统进行用户验证和历史记录管理。
🏗️ 系统架构
整体架构图
┌─────────────────┐ WebSocket ┌─────────────────┐ HTTP API ┌─────────────────┐
│ 前端界面 │ ◄─────────────► │ Tornado后端 │ ◄─────────────► │ Odoo系统 │
│ │ │ │ │ │
│ • HTML/CSS/JS │ │ • WebSocket处理 │ │ • 用户验证 │
│ • 流式渲染 │ │ • AI集成 │ │ • 历史记录 │
│ • 自动重连 │ │ • 会话管理 │ │ • 使用统计 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
📋 技术栈
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Python + Tornado | 高性能异步Web框架,原生支持WebSocket |
| AI集成 | OpenAI SDK + 阿里云通义千问 | 标准化AI接口,支持流式响应 |
| HTTP客户端 | httpx | 现代异步HTTP客户端库 |
| 前端技术 | HTML5 + CSS3 + JavaScript | 原生Web技术,无框架依赖 |
| 实时通信 | WebSocket API | 浏览器原生WebSocket支持 |
| 内容渲染 | Showdown.js | Markdown到HTML的转换 |
| 用户系统 | Odoo框架 | 企业级ERP系统,提供完整用户管理 |
🔄 数据流设计
- 用户认证流程: 前端提交用户ID → Tornado验证 → Odoo系统确认 → 建立会话
- 消息处理流程: 用户输入 → WebSocket传输 → AI处理 → 流式返回 → 历史记录保存
- 状态管理: 连接状态、用户信息、会话历史全程跟踪
实现效果


🔧 后端实现
1. 核心依赖和配置
重要说明: 以下代码展示了完整的生产级实现,包含错误处理、性能优化和安全考虑。
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.httpserver
from tornado.websocket import WebSocketHandler
import json
import datetime
import openai
import asyncio
import httpx
import sys
from openai import OpenAI
# AI客户端配置
client = OpenAI(
api_key='sk-your-api-key',
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 用户会话存储
user_sessions = {}
# Odoo API端点配置
ODOO_VERIFY_API = "http://localhost:8080/api/auth/verify_token"
ODOO_RESET_API = "http://localhost:8080/api/user/reset_usage"
ODOO_INCREMENT_API = "http://localhost:8080/api/user/increment_usage"
ODOO_CREATE_HISTORY_API = "http://localhost:8080/api/create/chat/message/api"
2. 工具函数
def _utc_iso_now():
"""返回UTC时间的ISO8601格式字符串"""
return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
def _to_iso_utc(dt: datetime.datetime):
"""将datetime对象转换为UTC时间的ISO格式"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=datetime.timezone.utc)
else:
dt = dt.astimezone(datetime.timezone.utc)
return dt.replace(microsecond=0).isoformat().replace("+00:00", "Z")
3. Odoo集成服务
async def verify_user_token(user_id: str):
"""验证用户Token"""
async with httpx.AsyncClient(timeout=10) as client:
try:
resp = await client.post(ODOO_VERIFY_API, json={"user_id": user_id})
if resp.status_code == 200:
raw = resp.json()
result = raw.get("result", {})
if result.get("code") == 200 and "user" in result:
return result["user"]
except Exception as e:
print("验证用户失败:", str(e))
return None
async def increment_usage_count(user_id):
"""增加用户使用次数"""
async with httpx.AsyncClient(timeout=10) as client:
try:
resp = await client.post(ODOO_INCREMENT_API, json={"user_id": user_id})
if resp.status_code == 200:
result = resp.json().get("result", {})
if result.get("code") != 200:
print("⚠️ 增加使用次数失败,返回码非 200:", result)
except Exception as e:
print("❌ 增加使用次数请求异常:", str(e))
async def create_history_record(*, user_id, question, answer, model_name, question_dt, answer_dt):
"""创建聊天历史记录"""
payload = {
"user_id": user_id,
"question": question,
"answer": answer,
"model_name": model_name,
"question_dt": question_dt,
"answer_dt": answer_dt,
}
async with httpx.AsyncClient(timeout=10) as client:
try:
resp = await client.post(ODOO_CREATE_HISTORY_API, json=payload)
if resp.status_code != 200:
print("⚠️ 创建历史记录失败:HTTP", resp.status_code, resp.text)
return
data = resp.json()
if data['result'].get("code") != 200:
print("⚠️ 创建历史记录失败:", data)
except Exception as e:
print("❌ 创建历史记录请求异常:", str(e))
4. WebSocket处理器
核心组件: 这是系统的心脏部分,处理所有WebSocket连接和消息路由。
class AIChatHandler(WebSocketHandler):
def open(self):
"""WebSocket连接建立时的处理"""
self.set_nodelay(True)
self.user_id = None
self.session = None
self.user_info = None
self.write_message(json.dumps({
"type": "info",
"data": "WebSocket AI Chat 已连接,欢迎开始对话"
}))
async def on_message(self, message):
"""处理接收到的消息"""
try:
data = json.loads(message)
msg_type = data.get("type")
user_id = data.get("user_id")
if not user_id:
await self.write_message(json.dumps({"type": "error", "data": "缺少用户信息"}))
return
# 验证用户
user = await verify_user_token(user_id)
if not user:
await self.write_message(json.dumps({"type": "error", "data": "用户不存在"}))
return
self.user_id = user["user_id"]
self.user_info = user
# 初始化用户会话
if self.user_id not in user_sessions:
user_sessions[self.user_id] = {"history": []}
self.session = user_sessions[self.user_id]
# 根据消息类型处理
if msg_type == "chat":
await self.handle_chat(data)
elif msg_type == "reset":
self.session["history"] = []
await self.write_message(json.dumps({"type": "info", "data": "会话已重置"}))
else:
await self.write_message(json.dumps({"type": "error", "data": "未知消息类型"}))
except Exception as e:
await self.write_message(json.dumps({"type": "error", "data": str(e)}))
async def handle_chat(self, data):
"""处理聊天消息"""
content = data.get("content")
model = data.get("model", "qwen-max")
if not content:
await self.write_message(json.dumps({"type": "error", "data": "消息内容为空"}))
return
# 记录提问时间
question_iso = _utc_iso_now()
# 每日用量检查
today = datetime.date.today().isoformat()
if not self.user_info.get("is_vip"):
if self.user_info.get("last_usage_date") != today:
await reset_usage_count(self.user_id)
self.user_info["daily_usage_count"] = 0
self.user_info["last_usage_date"] = today
if self.user_info["daily_usage_count"] >= 10:
await self.write_message(json.dumps({
"type": "limit",
"data": "普通用户每天只能使用 10 次,已达上限"
}))
return
# 加入会话历史
self.session["history"].append({"role": "user", "content": content})
try:
# 🚀 核心AI处理逻辑
# 调用AI流式接口
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是AI助手。"},
*self.session["history"]
],
temperature=0.3,
max_tokens=2048,
stream=True
)
# 流式返回AI回复
full_reply = ""
for chunk in response:
delta = chunk.choices[0].delta.content or ""
if delta:
full_reply += delta
await self.write_message(json.dumps({
"type": "stream",
"data": delta
}))
# 发送完成信号
await self.write_message(json.dumps({"type": "done"}))
self.session["history"].append({"role": "assistant", "content": full_reply})
# 更新用量统计
if not self.user_info.get("is_vip"):
self.user_info["daily_usage_count"] += 1
await increment_usage_count(self.user_id)
# 记录回答时间
answer_iso = _utc_iso_now()
# 保存聊天历史
await create_history_record(
user_id=self.user_id,
question=content,
answer=full_reply,
model_name=model,
question_dt=question_iso,
answer_dt=answer_iso
)
except Exception as e:
await self.write_message(json.dumps({
"type": "error",
"data": f"通义千问调用失败: {str(e)}"
}))
def check_origin(self, origin):
"""允许跨域连接"""
return True
5. Tornado应用和服务器启动
class AIApplication(tornado.web.Application):
def __init__(self):
handlers = [
(r"/AIChat", AIChatHandler)
]
settings = {
'debug': True,
}
super().__init__(handlers, **settings)
def main():
app = AIApplication()
server = tornado.httpserver.HTTPServer(app)
server.listen(9009)
print("✅ Tornado AI Chat WebSocket 服务运行中: ws://localhost:9009/AIChat")
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
main()
🎨 前端实现
1. HTML结构设计
设计理念: 采用现代化的响应式设计,提供直观的用户交互体验。
前端采用左右分栏布局,左侧为控制面板,右侧为聊天界面:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>🧠 Tornado AI Chat · 对话布局</title>
<script src="https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<!-- 左侧:连接与表单 -->
<aside class="panel">
<div class="brand">🧠 Tornado AI Chat</div>
<label>用户 ID(user_id)</label>
<input id="token" class="input" placeholder="例如:571" />
<label>预输入(可选)</label>
<textarea id="message" class="textarea" placeholder="例如:Odoo 是什么?"></textarea>
<button class="btn" onclick="sendMessage()">发送</button>
<button class="btn ghost" onclick="resetSession()">重置会话</button>
<div id="statusBar" class="status">状态:未连接</div>
</aside>
<!-- 右侧:对话主区 -->
<main class="main">
<div class="header">
<h2>对话</h2>
<div class="tip">支持 Markdown 流式渲染与代码样式。</div>
</div>
<div id="chatBox" class="stream"></div>
<div class="composer">
<div class="composer-inner">
<textarea id="composerInput" placeholder="输入消息,Enter 发送,Shift+Enter 换行"></textarea>
<button class="send" id="sendBtn">发送</button>
</div>
<div id="typing" class="typing" style="display:none;">AI 正在输入…</div>
</div>
</main>
</body>
</html>
2. CSS样式系统
视觉特色: 深色主题配合渐变背景,提供现代化的视觉体验。
采用CSS自定义属性和现代渐变设计,核心样式特点:
- 主题变量: 统一的颜色管理系统
- 渐变背景: 多层径向渐变营造科技感
- Grid布局: 响应式栅格系统
- 气泡样式: 区分用户和AI的消息样式
:root{
--bg-0:#0b1020; --bg-1:#0e1630;
--card:#111934cc; --stroke:#29335fa6;
--text:#e8eaf6; --sub:#9aa6d6;
--pri:#a46cff; --pri2:#6fe4ff;
--r:16px;
}
body{
margin:0; color:var(--text);
font-family: ui-sans-serif, system-ui, "Microsoft YaHei", Segoe UI, Roboto, Arial, "PingFang SC";
background:
radial-gradient(1200px 800px at 8% -10%, #2a1f50 0%, transparent 60%),
radial-gradient(1000px 700px at 110% 20%, #043d52 0%, transparent 60%),
linear-gradient(180deg, var(--bg-1), var(--bg-0));
display:grid;
grid-template-columns: 300px 1fr;
gap:18px; padding:18px;
}
.bubble{
max-width:min(78%, 720px);
border-radius:16px; padding:12px 14px; line-height:1.7;
border:1px solid rgba(255,255,255,.06);
box-shadow:0 12px 24px rgba(0,0,0,.25);
background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
}
.you .bubble{
background:
radial-gradient(120% 140% at 100% 0%, rgba(255,255,255,.12), transparent 40%),
linear-gradient(180deg, rgba(38,93,134,.55), rgba(21,38,78,.65));
border:1px solid rgba(111,228,255,.22);
}
.ai .bubble{
background:
radial-gradient(120% 140% at 0% 0%, rgba(255,255,255,.12), transparent 40%),
linear-gradient(180deg, rgba(124,73,190,.55), rgba(46,30,89,.65));
border:1px solid rgba(164,108,255,.22);
}
3. JavaScript WebSocket客户端
交互核心: 负责WebSocket连接管理、消息处理和流式渲染。
关键特性:
- 自动重连: 网络断开时自动重连机制
- 流式渲染: 实时显示AI回复内容
- Markdown支持: 自动解析和渲染Markdown格式
- 状态管理: 完整的连接状态跟踪
// 核心变量
let ws = null;
const chatBox = document.getElementById("chatBox");
const tokenInput = document.getElementById("token");
const composerInput = document.getElementById("composerInput");
const statusBar = document.getElementById("statusBar");
const typingEl = document.getElementById("typing");
const converter = new showdown.Converter({openLinksInNewWindow:true, tables:true});
// WebSocket连接管理
function connectWebSocket(){
ws = new WebSocket("ws://localhost:9009/AIChat");
ws.onopen = () => setStatus("✅ 已连接");
ws.onerror = () => setStatus("❌ 连接异常");
ws.onclose = () => {
setStatus("🔌 已断开,重连中…");
setTimeout(connectWebSocket, 1500);
};
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data || "{}");
if(msg.type === "stream"){
appendAssistantMessage(msg.data);
}
else if(msg.type === "done"){
finalizeAssistantMessage();
}
else if(msg.type === "limit"){
setStatus("⚠️ 限制:" + msg.data);
typingEl.style.display="none";
}
else if(msg.type === "error"){
setStatus("❌ 错误:" + msg.data);
typingEl.style.display="none";
}
else if(msg.type === "info"){
setStatus("ℹ️ " + msg.data);
}
};
}
// 消息发送
function send(content){
const token = tokenInput.value.trim();
if(!token || !content){ return; }
if(!ws || ws.readyState !== WebSocket.OPEN){
setStatus("WebSocket 未连接");
return;
}
appendUserMessage(content);
ws.send(JSON.stringify({
type:"chat",
user_id: token,
content,
model:"qwen-max"
}));
currentAssistantElement = null;
}
// 流式消息渲染
function appendAssistantMessage(mdText){
typingEl.style.display = "block";
maybeInsertDayDivider();
// 连续消息合并为同一组
if(!currentAssistantElement || lastSender !== 'ai'){
const {row, bubble} = rowEl('ai');
currentAssistantElement = bubble;
chatBox.appendChild(row);
}
const current = currentAssistantElement.innerText || "";
currentAssistantElement.innerHTML = converter.makeHtml(current + mdText);
chatBox.scrollTop = chatBox.scrollHeight;
lastSender = 'ai';
}
function finalizeAssistantMessage(){
if(currentAssistantElement){
const meta = document.createElement('div');
meta.className='meta';
meta.textContent = new Date().toLocaleTimeString();
currentAssistantElement.appendChild(meta);
}
currentAssistantElement = null;
typingEl.style.display = "none";
}
// 启动WebSocket连接
connectWebSocket();
🏢 Odoo后端集成
企业级支撑: 利用Odoo强大的用户管理和数据持久化能力。
1. 用户验证控制器
功能说明:
- 提供RESTful API接口
- 支持用户身份验证
- 管理用户使用统计
- 记录聊天历史数据
from datetime import datetime, timedelta, timezone
from odoo import http, fields
from odoo.http import request
class TokenAuthController(http.Controller):
@http.route('/api/auth/verify_token', type='json', auth='none', csrf=False)
def ai_verify_token(self, **kw):
data = request.jsonrequest
user_id = data.get("user_id")
if not user_id:
return {"code": 400, "msg": "缺少user_id"}
user = request.env['res.users'].sudo().search([('id', '=', user_id)], limit=1)
if not user:
return {"code": 401, "msg": "用户不存在"}
return {
"code": 200,
"msg": "验证通过",
"user": {
"user_id": user.id,
"name": user.name,
"login": user.login,
"is_vip": user.is_vip,
"daily_usage_count": user.daily_usage_count,
"last_usage_date": user.last_usage_date.strftime('%Y-%m-%d') if user.last_usage_date else ""
}
}
@http.route('/api/user/increment_usage', type='json', auth='none', csrf=False)
def increment_usage(self, **kw):
"""次数统计累加"""
data = request.jsonrequest
user_id = data.get("user_id")
user = request.env['res.users'].sudo().browse(user_id)
if user.exists():
user.write({
'daily_usage_count': user.daily_usage_count + 1,
'last_usage_date': datetime.now().strftime('%Y-%m-%d')
})
return {"code": 200}
return {"code": 404}
2. 历史记录管理
@http.route('/api/create/chat/message/api', type='json', auth='none', csrf=False, methods=['POST'])
def create_chat_message_usage(self, **kw):
"""创建聊天历史记录"""
data = request.jsonrequest or {}
# 读取参数
question = data.get('question')
answer = data.get('answer') or ''
model_name = data.get('model_name') or ''
user_id = data.get('user_id')
if not question:
return {"code": 400, "error": "Missing 'question'"}
# 解析用户
user = None
if user_id:
try:
user_id = int(user_id)
user = request.env['res.users'].sudo().browse(user_id)
if not user.exists():
return {"code": 404, "error": "User not found"}
except Exception:
return {"code": 400, "error": "Invalid 'user_id'"}
# 解析时间
q_dt = self._parse_dt(data.get('question_dt')) or fields.Datetime.now()
a_dt = self._parse_dt(data.get('answer_dt')) or False
# 创建历史记录
vals = {
"name": question,
"answer": answer,
"user_id": user.id if user else False,
"question_dt": q_dt,
"answer_dt": a_dt,
"model_name": model_name,
}
rec = request.env['ai.chat.history'].sudo().create(vals)
return {
"code": 200,
"id": rec.id,
"data": {
"question": rec.name,
"answer": rec.answer,
"user_id": rec.user_id.id if rec.user_id else None,
"question_dt": fields.Datetime.to_string(rec.question_dt),
"answer_dt": fields.Datetime.to_string(rec.answer_dt) if rec.answer_dt else None,
"model_name": rec.model_name,
}
}
⭐ 核心特性
1. 🚀 实时流式响应
| 特性 | 说明 | 技术实现 |
|---|---|---|
| 持久连接 | WebSocket长连接 | Tornado WebSocketHandler |
| 流式传输 | AI回复逐字显示 | OpenAI Stream API |
| Markdown渲染 | 支持代码高亮 | Showdown.js |
| 自动滚动 | 消息自动滚动到底部 | JavaScript DOM操作 |
2. 🔐 用户认证和权限控制
3. 💾 会话管理
- 内存缓存: 服务器端维护用户会话历史
- 上下文保持: 支持多轮对话的连续性
- 会话重置: 用户可随时清空对话历史
- 并发支持: 多用户同时在线聊天
4. 📊 历史记录持久化
# 数据结构示例
{
"question": "用户问题",
"answer": "AI回答",
"user_id": 123,
"model_name": "qwen-max",
"question_dt": "2025-01-15T10:30:00Z",
"answer_dt": "2025-01-15T10:30:15Z"
}
5. 🛡️ 错误处理和重连机制
- 异常捕获: 完善的try-catch覆盖
- 用户友好: 错误信息本地化显示
- 自动重连: 网络断开自动恢复连接
- 状态提示: 实时显示连接状态
🚀 部署说明
1. 环境要求
# Python 环境 (推荐 3.8+)
pip install tornado openai httpx asyncio
# 前端依赖 (CDN引入,无需安装)
# - Showdown.js (Markdown渲染)
2. 快速启动
# 1. 启动WebSocket服务
python TornadoWebsocketAIChat.py
# 2. 确保Odoo服务运行
# 默认端口: 8080
# 3. 浏览器访问前端页面
open ai_chat.html
3. 配置清单
| 配置项 | 位置 | 说明 |
|---|---|---|
| API密钥 | client = OpenAI(api_key=...) |
阿里云通义千问密钥 |
| WebSocket端口 | server.listen(9009) |
默认9009端口 |
| Odoo地址 | ODOO_*_API 变量 |
API端点配置 |
| CORS设置 | check_origin() |
跨域访问控制 |
4. 生产部署建议
- 负载均衡: 使用Nginx进行WebSocket代理
- SSL证书: 配置HTTPS/WSS安全连接
- 监控日志: 集成日志收集和监控系统
- 容器化: Docker部署提高可移植性
🔧 性能优化建议
1. 连接优化
# 连接池配置
httpx.AsyncClient(
timeout=10,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
)
# WebSocket心跳检测
async def heartbeat(self):
while True:
await asyncio.sleep(30) # 30秒心跳
if self.ws_connection and not self.ws_connection.closed:
await self.ws_connection.ping()
2. 缓存策略
# Redis缓存用户会话
import redis
redis_client = redis.asyncio.Redis(host='localhost', port=6379, decode_responses=True)
# 缓存用户信息
await redis_client.setex(f"user:{user_id}", 3600, json.dumps(user_info))
3. 安全增强
# 请求频率限制
from collections import defaultdict
import time
class RateLimiter:
def __init__(self, max_requests=10, time_window=60):
self.requests = defaultdict(list)
self.max_requests = max_requests
self.time_window = time_window
def is_allowed(self, user_id):
now = time.time()
user_requests = self.requests[user_id]
# 清理过期请求
user_requests[:] = [req_time for req_time in user_requests if now - req_time < self.time_window]
if len(user_requests) >= self.max_requests:
return False
user_requests.append(now)
return True
📝 总结
🎯 系统价值
本系统展示了如何使用现代Web技术构建高性能的实时AI聊天应用。通过WebSocket技术实现了真正的实时通信,
结合流式AI接口提供了优秀的用户体验。同时,与企业级Odoo系统的深度集成确保了用户管理和数据持久化的可靠性。
🔮 扩展方向
该方案具有良好的扩展性,可以轻松添加更多企业级功能:
| 功能模块 | 技术方案 | 应用场景 |
|---|---|---|
| 文件上传 | Multipart + Base64 | 文档问答、图片分析 |
| 多模态交互 | Vision API集成 | 图文混合对话 |
| 群聊支持 | Redis发布订阅 | 团队协作聊天 |
| 语音交互 | WebRTC + STT/TTS | 语音助手功能 |
| 移动端支持 | PWA + 响应式设计 | 跨平台使用 |
💡 最佳实践
- 性能优化: 合理使用连接池和缓存机制
- 安全防护: 实施速率限制和输入验证
- 监控告警: 建立完善的日志和监控体系
- 用户体验: 优化流式渲染和错误提示
🏆 适用场景
对于需要构建企业级AI聊天系统的开发者来说,这是一个完整且实用的技术参考,特别适合:
- 企业内部AI助手
- 客户服务机器人
- 知识问答系统
- 教育培训平台
🏷️ 文章标识
📂 分类: 实时通信技术 | 🔖 系列: AI应用开发
🎯 适合人群: 中高级开发者 | 💼 应用场景: 企业级AI聊天系统
⭐ 推荐指数: ★★★★★ | 🔄 更新频率: 根据技术发展定期更新
📞 联系方式
- 📧 邮箱: 13655699934@163.com
- 💬 微信: H13655699934
- 📱 公众号: 关注获取最新技术文章
开源协议: 本项目代码遵循MIT协议,欢迎贡献和改进!
技术交流: 如有问题,欢迎在GitHub Issues中讨论。
转载说明: 转载请注明出处并保留原文链接。

浙公网安备 33010602011771号