1.本章目标 #
本章将介绍系统设置模块的主要功能与实现思路。内容涵盖如何通过前端表单配置和保存各类参数(如向量模型、LLM模型、API Key、系统提示词、检索策略等),以及设置页面与后端接口的数据交互机制,帮助读者理解如何自定义和管理平台核心参数,实现智能问答、知识库检索等能力的灵活配置。
2.目录结构 #
// 项目根目录
rag-lite/
// 应用目录
├── app/
// 蓝图文件夹,包含各个业务模块
│ ├── blueprints/
// 蓝图包初始化文件
│ │ ├── __init__.py
// 认证相关蓝图
│ │ ├── auth.py
// 知识库相关蓝图
│ │ ├── knowledgebase.py
// 设置相关蓝图
│ │ ├── settings.py
// 工具类相关蓝图
│ │ └── utils.py
// ORM模型定义目录
│ ├── models/
// models包初始化文件
│ │ ├── __init__.py
// 基础模型
│ │ ├── base.py
// 聊天消息模型
│ │ ├── chat_message.py
// 聊天会话模型
│ │ ├── chat_session.py
// 文档模型
│ │ ├── document.py
// 知识库模型
│ │ ├── knowledgebase.py
// 设置模型
│ │ ├── settings.py
// 用户模型
│ │ └── user.py
// 业务服务层目录
│ ├── services/
// 存储服务相关目录
│ ├── storage/
// 存储包初始化文件
│ ├── __init__.py
// 存储基础类
│ ├── base.py
// 存储工厂
│ ├── factory.py
// 本地存储实现
│ ├── local_storage.py
// MinIO 对象存储实现
│ └── minio_storage.py
// 基础服务
│ ├── base_service.py
// 知识库服务
│ ├── knowledgebase_service.py
// 设置服务
│ ├── settings_service.py
// 存储服务
│ ├── storage_service.py
// 用户服务
│ └── user_service.py
// 静态文件目录
│ ├── static/
// 前端模板目录
│ ├── templates/
// 基础模板
│ ├── base.html
// 首页模板
│ ├── home.html
// 知识库列表页面
│ ├── kb_list.html
// 登录页面
│ ├── login.html
// 注册页面
│ ├── register.html
// 设置页面
│ └── settings.html
// 工具函数目录
│ ├── utils/
// 认证相关工具
│ ├── auth.py
// 数据库工具
│ ├── db.py
// 日志相关工具
│ ├── logger.py
// 模型配置工具
│ └── models_config.py
// 应用初始化文件
│ ├── __init__.py
// 配置文件
│ └── config.py
// 日志文件目录
├── logs/
// 主日志文件
│ └── rag_lite.log
// 存储文件夹
├── storages/
// 封面存储目录
│ └── covers/
// 项目主程序入口(Flask应用启动点)
├── main.py
// 项目开发计划
├── plan.md
// Python 项目依赖描述文件(PEP 621)
└── pyproject.toml3.页面渲染 #
3.1. settings.py #
app/blueprints/settings.py
"""
设置相关路由(视图 + API)
"""
from flask import Blueprint, render_template
from app.utils.auth import login_required
import logging
logger = logging.getLogger(__name__)
bp = Blueprint('settings', __name__)
@bp.route('/settings')
@login_required
def settings_view():
"""设置页面"""
return render_template('settings.html')3.2. settings.html #
app/templates/settings.html
{% extends "base.html" %}
{% block title %}设置 - RAG Lite{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">设置</li>
</ol>
</nav>
<h2><i class="bi bi-gear"></i> 系统设置</h2>
<p class="text-muted">配置模型、提示词和检索参数</p>
<form id="settingsForm" onsubmit="saveSettings(event)">
<!-- 标签页导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="embedding-tab" data-bs-toggle="tab" data-bs-target="#embedding" type="button" role="tab">
<i class="bi bi-diagram-3"></i> 向量嵌入模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="llm-tab" data-bs-toggle="tab" data-bs-target="#llm" type="button" role="tab">
<i class="bi bi-robot"></i> 大语言模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab" data-bs-target="#prompt" type="button" role="tab">
<i class="bi bi-chat-quote"></i> 提示词设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="retrieval-tab" data-bs-toggle="tab" data-bs-target="#retrieval" type="button" role="tab">
<i class="bi bi-search"></i> 检索设置
</button>
</li>
</ul>
<!-- 标签页内容 -->
<div class="tab-content" id="settingsTabContent">
<!-- 标签1: 向量嵌入模型 -->
<div class="tab-pane fade show active" id="embedding" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> 向量嵌入模型(Embedding)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="embeddingProvider" name="embedding_provider" onchange="updateEmbeddingForm()" required>
<option value="huggingface">HuggingFace</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择向量嵌入模型提供商</div>
</div>
<div class="mb-3" id="embeddingModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="embeddingModelName" name="embedding_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称或路径</div>
</div>
<div class="mb-3" id="embeddingApiKeyGroup" style="display: none;">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="embeddingApiKey" name="embedding_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="embeddingBaseUrlGroup" style="display: none;">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="embeddingBaseUrl" name="embedding_base_url" placeholder="例如: http://localhost:11434">
<div class="form-text">API Base URL(Ollama 需要)</div>
</div>
</div>
</div>
</div>
<!-- 标签2: 大语言模型 -->
<div class="tab-pane fade" id="llm" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-robot"></i> 大语言模型(LLM)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="llmProvider" name="llm_provider" onchange="updateLLMForm()" required>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择大语言模型提供商</div>
</div>
<div class="mb-3" id="llmModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="llmModelName" name="llm_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称</div>
</div>
<div class="mb-3" id="llmApiKeyGroup">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llmApiKey" name="llm_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="llmBaseUrlGroup">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="llmBaseUrl" name="llm_base_url" placeholder="例如: https://api.deepseek.com">
<div class="form-text">API Base URL</div>
</div>
<div class="mb-3">
<label class="form-label">温度 (Temperature)</label>
<input type="number" class="form-control" id="llmTemperature" name="llm_temperature"
value="0.7" step="0.1" min="0" max="2" placeholder="0.7">
<div class="form-text">控制输出的随机性,值越大越随机(0-2)</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 提示词设置 -->
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- 子标签页导航 -->
<ul class="nav nav-tabs mb-4" id="promptSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="chat-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#chat-prompt-sub" type="button" role="tab">
<i class="bi bi-chat"></i> 普通聊天提示词
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rag-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#rag-prompt-sub" type="button" role="tab">
<i class="bi bi-book"></i> 知识库聊天提示词
</button>
</li>
</ul>
<!-- 子标签页内容 -->
<div class="tab-content" id="promptSubTabContent">
<!-- 子标签1: 普通聊天提示词 -->
<div class="tab-pane fade show active" id="chat-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">普通聊天系统提示词</label>
<textarea class="form-control" id="chatSystemPrompt" name="chat_system_prompt" rows="10"
placeholder="输入普通聊天提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">普通聊天提示词用于指导AI助手在普通聊天(未选择知识库)时的回答风格和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量。</p>
</div>
</div>
</div>
<!-- 子标签2: 知识库聊天提示词 -->
<div class="tab-pane fade" id="rag-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">知识库聊天系统提示词</label>
<textarea class="form-control" id="ragSystemPrompt" name="rag_system_prompt" rows="6"
placeholder="输入知识库聊天系统提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">知识库聊天系统提示词用于在会话开始时设置AI助手的角色和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量(如 {context} 或 {question})。</p>
</div>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">知识库聊天查询提示词</label>
<textarea class="form-control" id="ragQueryPrompt" name="rag_query_prompt" rows="10"
placeholder="例如:文档内容: {context} 问题:{question} 请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。"></textarea>
<div class="form-text mt-2">
<p class="mb-1">知识库聊天查询提示词用于每次提问时构建提示,指导AI助手如何基于文档内容回答问题。</p>
<p class="mb-0"><strong>必须使用以下变量:</strong></p>
<ul class="mb-0">
<li><code>{context}</code> - 检索到的文档内容(必需)</li>
<li><code>{question}</code> - 用户的问题(必需)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 检索设置 -->
<div class="tab-pane fade" id="retrieval" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-search"></i> 检索设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">检索模式 <span class="text-danger">*</span></label>
<select class="form-select" id="retrievalMode" name="retrieval_mode" required>
<option value="vector">向量检索</option>
<option value="keyword">全文检索</option>
<option value="hybrid">混合检索</option>
</select>
<div class="form-text">选择文档检索方式</div>
</div>
<div class="mb-3" id="vectorThresholdGroup">
<label class="form-label">向量检索阈值</label>
<input type="number" class="form-control" id="vectorThreshold" name="vector_threshold"
value="0.2" step="0.1" min="0" max="1" placeholder="0.2">
<div class="form-text">向量相似度阈值,低于此值的文档将被过滤(0-1)</div>
</div>
<div class="mb-3" id="keywordThresholdGroup" style="display: none;">
<label class="form-label">全文检索阈值</label>
<input type="number" class="form-control" id="keywordThreshold" name="keyword_threshold"
value="0.5" step="0.1" min="0" max="1" placeholder="0.5">
<div class="form-text">关键词匹配阈值(0-1)</div>
</div>
<div class="mb-3" id="vectorWeightGroup" style="display: none;">
<label class="form-label">向量检索权重</label>
<input type="number" class="form-control" id="vectorWeight" name="vector_weight"
value="0.7" step="0.1" min="0" max="1" placeholder="0.7">
<div class="form-text">混合检索时向量检索的权重(0-1),关键词检索权重 = 1 - 向量权重</div>
</div>
<div class="mb-3">
<label class="form-label">TopN 结果数量</label>
<input type="number" class="form-control" id="topN" name="top_n"
value="5" min="1" max="50" placeholder="5">
<div class="form-text">返回的文档数量(1-50)</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>向量检索:</strong>基于语义相似度检索,适合理解问题意图</li>
<li><strong>全文检索:</strong>基于关键词匹配检索,适合精确匹配</li>
<li><strong>混合检索:</strong>结合向量和关键词检索,综合两者的优势</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" onclick="resetSettings()">重置</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存设置
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
</script>
{% endblock %}3.3. init.py #
app/init.py
# RAG Lite 应用模块说明
"""
RAG Lite Application
"""
# 导入操作系统相关模块
import os
# 从 Flask 包导入 Flask 应用对象
from flask import Flask
# 导入 Flask 跨域资源共享支持
from flask_cors import CORS
# 导入应用配置类
from app.config import Config
# 导入日志工具,用于获取日志记录器
from app.utils.logger import get_logger
# 导入数据库初始化函数
from app.utils.db import init_db
# 导入蓝图模块
+from app.blueprints import auth,knowledgebase,settings
# 导入获取当前用户信息函数
from app.utils.auth import get_current_user
# 定义创建 Flask 应用的工厂函数
def create_app(config_class=Config):
# 获取日志记录器,名称为当前模块名
logger = get_logger(__name__)
# 尝试初始化数据库
try:
# 输出日志,表示即将初始化数据库
logger.info("初始化数据库...")
# 执行数据库初始化函数
init_db()
# 输出日志,表示数据库初始化成功
logger.info("数据库初始化成功")
# 捕获任意异常
except Exception as e:
# 输出警告日志,提示数据库初始化失败,并输出异常信息
logger.warning(f"数据库初始化失败: {e}")
# 输出警告日志,提示检查数据库是否已存在,并建议手动创建数据表
logger.warning("请确认数据库已存在,或手动创建数据表")
# 创建 Flask 应用对象,并指定模板和静态文件目录
base_dir = os.path.abspath(os.path.dirname(__file__))
# 创建 Flask 应用对象,并指定模板和静态文件目录
app = Flask(
__name__,
# 指定模板文件目录
template_folder=os.path.join(base_dir, 'templates'),
# 指定静态文件目录
static_folder=os.path.join(base_dir, 'static')
)
# 从给定配置类加载配置信息到应用
app.config.from_object(config_class)
# 启用跨域请求支持
CORS(app)
# 记录应用创建日志信息
logger.info("Flask 应用已创建")
# 注册上下文处理器,使 current_user 在所有模板中可用
@app.context_processor
def inject_user():
# 返回当前用户信息字典
# 使用 get_current_user 获取当前用户信息,并将其添加到上下文字典中
# 这样在模板中可以直接使用 current_user 变量
return dict(current_user=get_current_user())
# 注册蓝图
app.register_blueprint(auth.bp)
# 注册知识库蓝图
app.register_blueprint(knowledgebase.bp)
# 注册设置蓝图
+ app.register_blueprint(settings.bp)
# 定义首页路由
@app.route('/')
def index():
return "Hello, World!"
# 返回已配置的 Flask 应用对象
return app
3.4. init.py #
app/blueprints/init.py
"""
蓝图模块
"""
+from app.blueprints import auth, settings
+__all__ = ['auth', 'settings']
4.嵌入模型 #
4.1. settings_service.py #
app/services/settings_service.py
"""
设置服务
"""
# 导入 Settings 模型
from app.models.settings import Settings
# 导入配置类
from app.config import Config
# 导入基础服务类
from app.services.base_service import BaseService
# 定义设置服务类,继承基础服务
class SettingsService(BaseService[Settings]):
"""设置服务"""
# 获取设置的方法(单例模式)
def get(self) -> dict:
"""
获取设置(单例模式)
Returns:
设置字典,如果不存在则返回默认值
"""
# 打开数据库会话
with self.session() as session:
# 查询主键为 'global' 的设置
settings = session.query(Settings).filter_by(id='global').first()
# 如果数据库中存在设置,返回其字典形式
if settings:
return settings.to_dict()
else:
# 如果不存在,返回默认设置
return self._get_default_settings()
# 获取默认设置的方法
def _get_default_settings(self) -> dict:
"""获取默认设置"""
# 返回包含所有默认字段值的字典
return {
'id': 'global', # 设置主键
'embedding_provider': 'huggingface', # 默认 embedding provider
'embedding_model_name': 'sentence-transformers/all-MiniLM-L6-v2', # 默认 embedding 模型
'embedding_api_key': None, # 默认无 embedding API key
'embedding_base_url': None, # 默认无 embedding base url
'llm_provider': 'deepseek', # 默认 LLM provider
'llm_model_name': Config.DEEPSEEK_CHAT_MODEL, # 默认 LLM 模型
'llm_api_key': Config.DEEPSEEK_API_KEY, # 配置里的默认 LLM API key
'llm_base_url': Config.DEEPSEEK_BASE_URL, # 配置里的默认 LLM base url
'llm_temperature': '0.7', # 默认温度
'chat_system_prompt': '你是一个专业的AI助手。请友好、准确地回答用户的问题。', # 聊天系统默认提示词
'rag_system_prompt': '你是一个专业的AI助手。请基于文档内容回答问题。', # RAG系统提示词
'rag_query_prompt': '文档内容:\n{context}\n\n问题:{question}\n\n请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。', # RAG查询提示词
'retrieval_mode': 'vector', # 默认检索模式
'vector_threshold': '0.2', # 向量检索阈值
'keyword_threshold': '0.5', # 关键词检索阈值
'vector_weight': '0.7', # 检索混合权重
'top_n': '5', # 返回结果数量
'created_at': None, # 创建时间
'updated_at': None # 更新时间
}
# 用于更新设置的方法
def update(self, data: dict) -> dict:
"""
更新设置
Args:
data: 设置数据
Returns:
更新后的设置
"""
# 启动事务会话
with self.transaction() as session:
# 查询主键为 'global' 的设置
settings = session.query(Settings).filter_by(id='global').first()
# 如果已存在设置,则逐项更新
if settings:
# 遍历提交的所有字段及其对应的值
for key, value in data.items():
# 如果设置中有该属性且值不是None,则进行更新
if hasattr(settings, key) and value is not None:
setattr(settings, key, value)
else:
# 不存在则新建一个 Settings 对象
settings = Settings(id='global')
# 设置各属性
for key, value in data.items():
if hasattr(settings, key) and value is not None:
setattr(settings, key, value)
# 添加到会话
session.add(settings)
# flush 保证所有更改同步到数据库会话(提交之前,保证主键自增/更新时间等有效)
session.flush()
# refresh 保证 settings 对象数据是数据库最新的内容(例如 updated_at 字段)
session.refresh(settings)
# 返回已更新的设置字典
return settings.to_dict()
# 实例化设置服务
settings_service = SettingsService() 4.2. models_config.py #
app/utils/models_config.py
# 模型配置
# 定义可用的 Embedding 模型和 LLM 模型列表
"""
模型配置
定义可用的 Embedding 模型和 LLM 模型列表
"""
# 定义向量嵌入模型(Embedding Models)的配置字典
EMBEDDING_MODELS = {
# HuggingFace 嵌入模型
'huggingface': {
# 名称
'name': 'HuggingFace Embeddings',
# 描述说明
'description': '本地 HuggingFace 模型',
# 可用模型列表
'models': [
# 第一个模型:all-MiniLM-L6-v2
{
# 模型名称
'name': 'sentence-transformers/all-MiniLM-L6-v2',
# 模型路径
'path': 'sentence-transformers/all-MiniLM-L6-v2',
# 向量维度
'dimension': '384',
# 描述
'description': '轻量级多语言模型,速度快'
},
# 第二个模型:paraphrase-multilingual-MiniLM-L12-v2
{
'name': 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2',
'path': 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2',
'dimension': '384',
'description': '多语言模型,支持中文'
},
# 第三个模型:bge-small-zh-v1.5
{
'name': 'BAAI/bge-small-zh-v1.5',
'path': 'BAAI/bge-small-zh-v1.5',
'dimension': '512',
'description': '中文优化模型'
}
],
# 是否需要 API Key
'requires_api_key': False,
# 是否需要 Base URL
'requires_base_url': False
},
# OpenAI 嵌入模型
'openai': {
'name': 'OpenAI Embeddings',
'description': 'OpenAI 官方嵌入模型',
# OpenAI 可用模型
'models': [
{
'name': 'text-embedding-3-small',
'dimension': '1536',
'description': '小型模型,速度快'
},
{
'name': 'text-embedding-3-large',
'dimension': '3072',
'description': '大型模型,精度高'
},
{
'name': 'text-embedding-ada-002',
'dimension': '1536',
'description': '经典模型'
}
],
'requires_api_key': True,
'requires_base_url': False
},
# 本地 Ollama 嵌入模型
'ollama': {
'name': 'Ollama Embeddings',
'description': '本地 Ollama 模型',
# Ollama 可用嵌入模型
'models': [
{
'name': 'nomic-embed-text',
'dimension': '768',
'description': '通用文本嵌入模型'
}
],
'requires_api_key': False,
'requires_base_url': True
}
}
# 定义 LLM(大模型,推理/对话模型)配置字典
LLM_MODELS = {
# DeepSeek 模型配置
'deepseek': {
'name': 'DeepSeek',
'description': 'DeepSeek API',
# DeepSeek 可用模型列表
'models': [
# DeepSeek 对话模型
{
'name': 'deepseek-chat',
'description': '对话模型'
},
# DeepSeek 代码模型
{
'name': 'deepseek-coder',
'description': '代码模型'
}
],
'requires_api_key': True,
'requires_base_url': True
},
# OpenAI 大模型配置
'openai': {
'name': 'OpenAI',
'description': 'OpenAI API',
# OpenAI 可用大模型
'models': [
{
'name': 'gpt-4',
'description': 'GPT-4 模型'
},
{
'name': 'gpt-4-turbo',
'description': 'GPT-4 Turbo 模型'
},
{
'name': 'gpt-3.5-turbo',
'description': 'GPT-3.5 Turbo 模型'
}
],
'requires_api_key': True,
'requires_base_url': False
},
# 本地 Ollama 大模型配置
'ollama': {
'name': 'Ollama',
'description': '本地 Ollama 模型',
# Ollama 可用大模型
'models': [
{
'name': 'llama2',
'description': 'Llama 2 模型'
},
{
'name': 'mistral',
'description': 'Mistral 模型'
},
{
'name': 'qwen',
'description': 'Qwen 模型'
}
],
'requires_api_key': False,
'requires_base_url': True
}
}
4.3. settings.py #
app/blueprints/settings.py
# 设置相关路由(视图 + API)
"""
设置相关路由(视图 + API)
"""
# 导入 Flask 蓝图和模板渲染函数
from flask import Blueprint, render_template
# 导入登录校验装饰器
from app.utils.auth import login_required
# 导入标准化响应、错误处理装饰器、请求JSON体校验工具
+from app.blueprints.utils import success_response, handle_api_error, require_json_body
# 导入嵌入模型、LLM模型配置
+from app.utils.models_config import EMBEDDING_MODELS, LLM_MODELS
# 导入设置服务
+from app.services.settings_service import settings_service
# 导入日志模块
import logging
# 获取当前模块的logger对象
logger = logging.getLogger(__name__)
# 创建 settings 蓝图
bp = Blueprint('settings', __name__)
# 注册视图路由:设置页面
@bp.route('/settings')
# 要求登录
@login_required
def settings_view():
"""设置页面"""
# 渲染 settings.html 模板页面
+ return render_template('settings.html')
# 注册 API 路由:获取可用模型列表
+@bp.route('/api/v1/settings/models', methods=['GET'])
# 错误处理装饰器
+@handle_api_error
+def api_get_available_models():
+ """获取可用的模型列表"""
# 返回嵌入模型与LLM可用模型数据
+ return success_response({
+ 'embedding_models': EMBEDDING_MODELS,
+ 'llm_models': LLM_MODELS
+ })
# 注册 API 路由:更新设置
+@bp.route('/api/v1/settings', methods=['PUT'])
# 错误处理装饰器
+@handle_api_error
+def api_update_settings():
+ """更新设置"""
# 校验请求体并获取数据
+ data, err = require_json_body()
# 如果有错误,直接返回错误响应
+ if err:
+ return err
# 调用设置服务进行更新
+ settings = settings_service.update(data)
# 返回更新后的设置
+ return success_response(settings, "Settings updated successfully")
# 注册 API 路由:获取当前设置
+@bp.route('/api/v1/settings', methods=['GET'])
# 错误处理装饰器
+@handle_api_error
+def api_get_settings():
+ """获取设置"""
# 从设置服务获取当前设置
+ settings = settings_service.get()
# 返回设置数据
+ return success_response(settings) 4.4. utils.py #
app/blueprints/utils.py
"""
路由工具函数
"""
# 导入Flask用于返回JSON响应
from flask import jsonify,request
# 导入装饰器工具,用来保持原函数信息
from functools import wraps
# 导入类型提示工具
from typing import Tuple, Optional
# 导入获取当前用户的工具函数
from app.utils.auth import get_current_user
# 导入日志模块
import logging
# 获取logger对象(当前模块名)
logger = logging.getLogger(__name__)
# 定义成功响应函数
def success_response(data=None, message="success"):
"""
成功响应
Args:
data: 响应数据
message: 响应消息
Returns:
JSON 响应
"""
# 返回标准格式的JSON成功响应
return jsonify({
"code": 200, # 状态码200,表示成功
"message": message, # 响应消息
"data": data # 响应数据
})
# 定义错误响应函数
def error_response(message: str, code: int = 400):
"""
错误响应
Args:
message: 错误消息
code: HTTP 状态码
Returns:
JSON 响应和状态码
"""
# 返回标准格式的JSON错误响应,以及相应的HTTP状态码
return jsonify({
"code": code, # 错误码,对应HTTP状态码
"message": message, # 错误消息
"data": None # 错误时无数据
}), code
# 定义API错误处理装饰器
def handle_api_error(func):
"""
API 错误处理装饰器
使用示例:
@handle_api_error
def my_api():
# API 逻辑
return success_response(data)
"""
# 保留原函数信息并定义包装器
@wraps(func)
def wrapper(*args, **kwargs):
try:
# 正常执行被装饰的API函数
return func(*args, **kwargs)
except ValueError as e:
# 捕获ValueError,日志记录warning信息并返回400错误响应
logger.warning(f"ValueError in {func.__name__}: {e}")
return error_response(str(e), 400)
except Exception as e:
# 捕获其他所有异常,日志记录error信息并返回500错误响应
logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
return error_response(str(e), 500)
# 返回包装后的函数
return wrapper
# 定义获取分页参数的函数,允许指定最大每页数量
def get_pagination_params(max_page_size: int = 1000) -> Tuple[int, int]:
"""
获取分页参数
Args:
max_page_size: 最大每页数量
Returns:
(page, page_size) 元组
"""
# 获取请求中的 'page' 参数,默认为1,并将其转换为整数
page = int(request.args.get('page', 1))
# 获取请求中的 'page_size' 参数,默认为10,并将其转换为整数
page_size = int(request.args.get('page_size', 10))
# 保证 page 至少为1
page = max(1, page)
# 保证 page_size 至少为1且不超过 max_page_size
page_size = max(1, min(page_size, max_page_size))
# 返回分页的(page, page_size)元组
return page, page_size
# 定义获取当前用户或返回错误的函数
def get_current_user_or_error():
"""
获取当前用户,如果未登录则返回错误响应
Returns:
如果成功返回 (user_dict, None),如果失败返回 (None, error_response)
"""
# 调用 get_current_user() 获取当前用户对象
current_user = get_current_user()
# 如果没有获取到用户,则返回 (None, 错误响应)
if not current_user:
return None, error_response("Unauthorized", 401)
# 如果获取到用户,则返回 (用户对象, None)
return current_user, None
# 定义检查资源所有权的函数,判断当前用户是否为资源所有者
def check_ownership(entity_user_id: str, current_user_id: str,
entity_name: str = "资源") -> Tuple[bool, Optional[Tuple]]:
# 检查资源所属用户ID是否与当前用户ID相同
if entity_user_id != current_user_id:
# 如果不同,返回False,并返回403未授权的错误响应
return False, error_response(f"Unauthorized to access this {entity_name}", 403)
# 如果相同,则有权限,返回True和None
return True, None
# 定义函数:检查请求体是否为 JSON
+def require_json_body():
+ """
+ 检查请求是否有 JSON 体
+ Returns:
+ 如果存在返回 (data, None),如果不存在返回 (None, error_response)
+ """
# 从请求中获取 JSON 数据
+ data = request.get_json()
# 如果没有获取到数据,则返回错误响应
+ if not data:
+ return None, error_response("Request body is required", 400)
# 如果获取到了数据,则返回数据和None表示没有错误
+ return data, None4.5. settings.html #
app/templates/settings.html
{% extends "base.html" %}
{% block title %}设置 - RAG Lite{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">设置</li>
</ol>
</nav>
<h2><i class="bi bi-gear"></i> 系统设置</h2>
<p class="text-muted">配置模型、提示词和检索参数</p>
<form id="settingsForm" onsubmit="saveSettings(event)">
<!-- 标签页导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="embedding-tab" data-bs-toggle="tab" data-bs-target="#embedding" type="button" role="tab">
<i class="bi bi-diagram-3"></i> 向量嵌入模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="llm-tab" data-bs-toggle="tab" data-bs-target="#llm" type="button" role="tab">
<i class="bi bi-robot"></i> 大语言模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab" data-bs-target="#prompt" type="button" role="tab">
<i class="bi bi-chat-quote"></i> 提示词设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="retrieval-tab" data-bs-toggle="tab" data-bs-target="#retrieval" type="button" role="tab">
<i class="bi bi-search"></i> 检索设置
</button>
</li>
</ul>
<!-- 标签页内容 -->
<div class="tab-content" id="settingsTabContent">
<!-- 标签1: 向量嵌入模型 -->
<div class="tab-pane fade show active" id="embedding" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> 向量嵌入模型(Embedding)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="embeddingProvider" name="embedding_provider" onchange="updateEmbeddingForm()" required>
<option value="huggingface">HuggingFace</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择向量嵌入模型提供商</div>
</div>
<div class="mb-3" id="embeddingModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="embeddingModelName" name="embedding_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称或路径</div>
</div>
<div class="mb-3" id="embeddingApiKeyGroup" style="display: none;">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="embeddingApiKey" name="embedding_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="embeddingBaseUrlGroup" style="display: none;">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="embeddingBaseUrl" name="embedding_base_url" placeholder="例如: http://localhost:11434">
<div class="form-text">API Base URL(Ollama 需要)</div>
</div>
</div>
</div>
</div>
<!-- 标签2: 大语言模型 -->
<div class="tab-pane fade" id="llm" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-robot"></i> 大语言模型(LLM)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="llmProvider" name="llm_provider" onchange="updateLLMForm()" required>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择大语言模型提供商</div>
</div>
<div class="mb-3" id="llmModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="llmModelName" name="llm_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称</div>
</div>
<div class="mb-3" id="llmApiKeyGroup">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llmApiKey" name="llm_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="llmBaseUrlGroup">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="llmBaseUrl" name="llm_base_url" placeholder="例如: https://api.deepseek.com">
<div class="form-text">API Base URL</div>
</div>
<div class="mb-3">
<label class="form-label">温度 (Temperature)</label>
<input type="number" class="form-control" id="llmTemperature" name="llm_temperature"
value="0.7" step="0.1" min="0" max="2" placeholder="0.7">
<div class="form-text">控制输出的随机性,值越大越随机(0-2)</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 提示词设置 -->
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- 子标签页导航 -->
<ul class="nav nav-tabs mb-4" id="promptSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="chat-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#chat-prompt-sub" type="button" role="tab">
<i class="bi bi-chat"></i> 普通聊天提示词
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rag-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#rag-prompt-sub" type="button" role="tab">
<i class="bi bi-book"></i> 知识库聊天提示词
</button>
</li>
</ul>
<!-- 子标签页内容 -->
<div class="tab-content" id="promptSubTabContent">
<!-- 子标签1: 普通聊天提示词 -->
<div class="tab-pane fade show active" id="chat-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">普通聊天系统提示词</label>
<textarea class="form-control" id="chatSystemPrompt" name="chat_system_prompt" rows="10"
placeholder="输入普通聊天提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">普通聊天提示词用于指导AI助手在普通聊天(未选择知识库)时的回答风格和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量。</p>
</div>
</div>
</div>
<!-- 子标签2: 知识库聊天提示词 -->
<div class="tab-pane fade" id="rag-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">知识库聊天系统提示词</label>
<textarea class="form-control" id="ragSystemPrompt" name="rag_system_prompt" rows="6"
placeholder="输入知识库聊天系统提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">知识库聊天系统提示词用于在会话开始时设置AI助手的角色和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量(如 {context} 或 {question})。</p>
</div>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">知识库聊天查询提示词</label>
<textarea class="form-control" id="ragQueryPrompt" name="rag_query_prompt" rows="10"
placeholder="例如:文档内容: {context} 问题:{question} 请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。"></textarea>
<div class="form-text mt-2">
<p class="mb-1">知识库聊天查询提示词用于每次提问时构建提示,指导AI助手如何基于文档内容回答问题。</p>
<p class="mb-0"><strong>必须使用以下变量:</strong></p>
<ul class="mb-0">
<li><code>{context}</code> - 检索到的文档内容(必需)</li>
<li><code>{question}</code> - 用户的问题(必需)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 检索设置 -->
<div class="tab-pane fade" id="retrieval" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-search"></i> 检索设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">检索模式 <span class="text-danger">*</span></label>
<select class="form-select" id="retrievalMode" name="retrieval_mode" required>
<option value="vector">向量检索</option>
<option value="keyword">全文检索</option>
<option value="hybrid">混合检索</option>
</select>
<div class="form-text">选择文档检索方式</div>
</div>
<div class="mb-3" id="vectorThresholdGroup">
<label class="form-label">向量检索阈值</label>
<input type="number" class="form-control" id="vectorThreshold" name="vector_threshold"
value="0.2" step="0.1" min="0" max="1" placeholder="0.2">
<div class="form-text">向量相似度阈值,低于此值的文档将被过滤(0-1)</div>
</div>
<div class="mb-3" id="keywordThresholdGroup" style="display: none;">
<label class="form-label">全文检索阈值</label>
<input type="number" class="form-control" id="keywordThreshold" name="keyword_threshold"
value="0.5" step="0.1" min="0" max="1" placeholder="0.5">
<div class="form-text">关键词匹配阈值(0-1)</div>
</div>
<div class="mb-3" id="vectorWeightGroup" style="display: none;">
<label class="form-label">向量检索权重</label>
<input type="number" class="form-control" id="vectorWeight" name="vector_weight"
value="0.7" step="0.1" min="0" max="1" placeholder="0.7">
<div class="form-text">混合检索时向量检索的权重(0-1),关键词检索权重 = 1 - 向量权重</div>
</div>
<div class="mb-3">
<label class="form-label">TopN 结果数量</label>
<input type="number" class="form-control" id="topN" name="top_n"
value="5" min="1" max="50" placeholder="5">
<div class="form-text">返回的文档数量(1-50)</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>向量检索:</strong>基于语义相似度检索,适合理解问题意图</li>
<li><strong>全文检索:</strong>基于关键词匹配检索,适合精确匹配</li>
<li><strong>混合检索:</strong>结合向量和关键词检索,综合两者的优势</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" onclick="resetSettings()">重置</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存设置
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
+// 监听页面加载完成后,执行异步函数
+document.addEventListener('DOMContentLoaded', async function() {
+ try {
+ // 发送请求获取可用模型列表
+ const modelsResponse = await fetch('/api/v1/settings/models');
+ // 解析返回的JSON数据
+ const modelsResult = await modelsResponse.json();
+ // 如果请求成功(响应码为200)
+ if (modelsResult.code === 200) {
+ // 取得可用模型数据
+ const availableModels = modelsResult.data;
+ // 更新嵌入模型表单
+ updateEmbeddingForm(availableModels);
+ // 发送请求获取当前设置
+ const settingsResponse = await fetch('/api/v1/settings');
+ // 解析返回的JSON数据
+ const settingsResult = await settingsResponse.json();
+ // 如果请求成功(响应码为200)
+ if (settingsResult.code === 200) {
+ // 加载设置到页面
+ loadSettings(settingsResult.data);
+ }
+ }
+ } catch (error) {
+ // 捕获异常并在控制台打印错误信息
+ console.error('加载设置失败:', error);
+ // 弹窗提示加载失败
+ alert('加载设置失败: ' + error.message);
+ }
+});
+// 加载设置并填充到表单
+function loadSettings(settings) {
+ // 设置嵌入向量提供商,默认为huggingface
+ document.getElementById('embeddingProvider').value = settings.embedding_provider || 'huggingface';
+ // 如果有嵌入模型名称,设置模型名称
+ if (settings.embedding_model_name) {
+ document.getElementById('embeddingModelName').value = settings.embedding_model_name;
+ }
+ // 设置API Key,如果没有则为空字符串
+ document.getElementById('embeddingApiKey').value = settings.embedding_api_key || '';
+ // 设置Base URL,如果没有则为空字符串
+ document.getElementById('embeddingBaseUrl').value = settings.embedding_base_url || '';
+}
+// 更新嵌入模型表单(根据可用模型动态渲染选项)
+function updateEmbeddingForm(availableModels) {
+ // 获取当前选中的提供商
+ const provider = document.getElementById('embeddingProvider').value;
+ // 获取模型选择框
+ const modelSelect = document.getElementById('embeddingModelName');
+ // 获取API Key输入框所在元素
+ const apiKeyGroup = document.getElementById('embeddingApiKeyGroup');
+ // 获取Base URL输入框所在元素
+ const baseUrlGroup = document.getElementById('embeddingBaseUrlGroup');
+ // 记住当前选中的模型值(以便刷新后尽量保留)
+ const currentValue = modelSelect.value;
+ // 清空模型下拉框内容,并加一个提示选项
+ modelSelect.innerHTML = '<option value="">请选择模型</option>';
+ // 如果存在可用模型数据,并且当前提供商有对应条目
+ if (availableModels && availableModels.embedding_models[provider]) {
+ // 获取该提供商的详细信息
+ const providerInfo = availableModels.embedding_models[provider];
+ // 遍历所有模型,添加为下拉选项
+ providerInfo.models.forEach(model => {
+ // 创建一个option元素
+ const option = document.createElement('option');
+ // 选项的值为path属性,否则为name
+ const optionValue = model.path || model.name;
+ option.value = optionValue;
+ // 构造显示文本,包括名称、维度和描述
+ const displayText = model.name + (model.dimension ? ` (维度: ${model.dimension})` : '') + (model.description ? ' - ' + model.description : '');
+ option.textContent = displayText;
+ // 添加到模型下拉框
+ modelSelect.appendChild(option);
+ });
+ // 恢复刷新前选中的值(如果还有)
+ if (currentValue) {
+ // 检查当前值是否还在选项中
+ const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
+ if (optionExists) {
+ // 如果存在,直接选中
+ modelSelect.value = currentValue;
+ } else {
+ // 如果不存在,尝试“模糊匹配”
+ const matchingOption = Array.from(modelSelect.options).find(opt => {
+ const optValue = opt.value;
+ // 判断当前值和选项值是否完全相等或相互包含
+ return optValue === currentValue || optValue.includes(currentValue) || currentValue.includes(optValue);
+ });
+ // 如果找到匹配项,则选中该项
+ if (matchingOption) {
+ modelSelect.value = matchingOption.value;
+ }
+ }
+ }
+ // 根据提供商是否需要API Key,显示或隐藏输入框
+ if (providerInfo.requires_api_key) {
+ apiKeyGroup.style.display = 'block';
+ document.getElementById('embeddingApiKey').required = true;
+ } else {
+ apiKeyGroup.style.display = 'none';
+ document.getElementById('embeddingApiKey').required = false;
+ }
+ // 根据提供商是否需要Base URL,显示或隐藏输入框
+ if (providerInfo.requires_base_url) {
+ baseUrlGroup.style.display = 'block';
+ } else {
+ baseUrlGroup.style.display = 'none';
+ }
+ }
+}
+// 保存设置(表单提交处理函数,异步提交到后端)
+async function saveSettings(event) {
+ // 阻止表单的默认提交行为
+ event.preventDefault();
+ // 获取表单dom对象
+ const form = event.target;
+ // 构造FormData对象用于取值
+ const formData = new FormData(form);
+ // 构造要提交的数据对象
+ const data = {
+ embedding_provider: formData.get('embedding_provider'),
+ embedding_model_name: formData.get('embedding_model_name') || null,
+ embedding_api_key: formData.get('embedding_api_key') || null,
+ embedding_base_url: formData.get('embedding_base_url') || null
+ };
+ try {
+ // 发送PUT请求到设置API,提交JSON数据
+ const response = await fetch('/api/v1/settings', {
+ method: 'PUT',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify(data)
+ });
+ // 解析返回结果
+ const result = await response.json();
+ // 如果提交成功
+ if (response.ok) {
+ alert('设置保存成功!');
+ // 刷新页面
+ location.reload();
+ } else {
+ // 否则弹窗提示失败原因
+ alert('保存失败: ' + result.message);
+ }
+ } catch (error) {
+ // 捕获异常并弹窗提示
+ alert('保存失败: ' + error.message);
+ }
+}
</script>
{% endblock %}
5.大模型设置 #
5.1. settings.html #
app/templates/settings.html
{% extends "base.html" %}
{% block title %}设置 - RAG Lite{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">设置</li>
</ol>
</nav>
<h2><i class="bi bi-gear"></i> 系统设置</h2>
<p class="text-muted">配置模型、提示词和检索参数</p>
<form id="settingsForm" onsubmit="saveSettings(event)">
<!-- 标签页导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="embedding-tab" data-bs-toggle="tab" data-bs-target="#embedding" type="button" role="tab">
<i class="bi bi-diagram-3"></i> 向量嵌入模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="llm-tab" data-bs-toggle="tab" data-bs-target="#llm" type="button" role="tab">
<i class="bi bi-robot"></i> 大语言模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab" data-bs-target="#prompt" type="button" role="tab">
<i class="bi bi-chat-quote"></i> 提示词设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="retrieval-tab" data-bs-toggle="tab" data-bs-target="#retrieval" type="button" role="tab">
<i class="bi bi-search"></i> 检索设置
</button>
</li>
</ul>
<!-- 标签页内容 -->
<div class="tab-content" id="settingsTabContent">
<!-- 标签1: 向量嵌入模型 -->
<div class="tab-pane fade show active" id="embedding" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> 向量嵌入模型(Embedding)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="embeddingProvider" name="embedding_provider" onchange="updateEmbeddingForm()" required>
<option value="huggingface">HuggingFace</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择向量嵌入模型提供商</div>
</div>
<div class="mb-3" id="embeddingModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="embeddingModelName" name="embedding_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称或路径</div>
</div>
<div class="mb-3" id="embeddingApiKeyGroup" style="display: none;">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="embeddingApiKey" name="embedding_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="embeddingBaseUrlGroup" style="display: none;">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="embeddingBaseUrl" name="embedding_base_url" placeholder="例如: http://localhost:11434">
<div class="form-text">API Base URL(Ollama 需要)</div>
</div>
</div>
</div>
</div>
<!-- 标签2: 大语言模型 -->
<div class="tab-pane fade" id="llm" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-robot"></i> 大语言模型(LLM)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="llmProvider" name="llm_provider" onchange="updateLLMForm()" required>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择大语言模型提供商</div>
</div>
<div class="mb-3" id="llmModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="llmModelName" name="llm_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称</div>
</div>
<div class="mb-3" id="llmApiKeyGroup">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llmApiKey" name="llm_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="llmBaseUrlGroup">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="llmBaseUrl" name="llm_base_url" placeholder="例如: https://api.deepseek.com">
<div class="form-text">API Base URL</div>
</div>
<div class="mb-3">
<label class="form-label">温度 (Temperature)</label>
<input type="number" class="form-control" id="llmTemperature" name="llm_temperature"
value="0.7" step="0.1" min="0" max="2" placeholder="0.7">
<div class="form-text">控制输出的随机性,值越大越随机(0-2)</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 提示词设置 -->
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- 子标签页导航 -->
<ul class="nav nav-tabs mb-4" id="promptSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="chat-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#chat-prompt-sub" type="button" role="tab">
<i class="bi bi-chat"></i> 普通聊天提示词
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rag-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#rag-prompt-sub" type="button" role="tab">
<i class="bi bi-book"></i> 知识库聊天提示词
</button>
</li>
</ul>
<!-- 子标签页内容 -->
<div class="tab-content" id="promptSubTabContent">
<!-- 子标签1: 普通聊天提示词 -->
<div class="tab-pane fade show active" id="chat-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">普通聊天系统提示词</label>
<textarea class="form-control" id="chatSystemPrompt" name="chat_system_prompt" rows="10"
placeholder="输入普通聊天提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">普通聊天提示词用于指导AI助手在普通聊天(未选择知识库)时的回答风格和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量。</p>
</div>
</div>
</div>
<!-- 子标签2: 知识库聊天提示词 -->
<div class="tab-pane fade" id="rag-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">知识库聊天系统提示词</label>
<textarea class="form-control" id="ragSystemPrompt" name="rag_system_prompt" rows="6"
placeholder="输入知识库聊天系统提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">知识库聊天系统提示词用于在会话开始时设置AI助手的角色和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量(如 {context} 或 {question})。</p>
</div>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">知识库聊天查询提示词</label>
<textarea class="form-control" id="ragQueryPrompt" name="rag_query_prompt" rows="10"
placeholder="例如:文档内容: {context} 问题:{question} 请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。"></textarea>
<div class="form-text mt-2">
<p class="mb-1">知识库聊天查询提示词用于每次提问时构建提示,指导AI助手如何基于文档内容回答问题。</p>
<p class="mb-0"><strong>必须使用以下变量:</strong></p>
<ul class="mb-0">
<li><code>{context}</code> - 检索到的文档内容(必需)</li>
<li><code>{question}</code> - 用户的问题(必需)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 检索设置 -->
<div class="tab-pane fade" id="retrieval" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-search"></i> 检索设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">检索模式 <span class="text-danger">*</span></label>
<select class="form-select" id="retrievalMode" name="retrieval_mode" required>
<option value="vector">向量检索</option>
<option value="keyword">全文检索</option>
<option value="hybrid">混合检索</option>
</select>
<div class="form-text">选择文档检索方式</div>
</div>
<div class="mb-3" id="vectorThresholdGroup">
<label class="form-label">向量检索阈值</label>
<input type="number" class="form-control" id="vectorThreshold" name="vector_threshold"
value="0.2" step="0.1" min="0" max="1" placeholder="0.2">
<div class="form-text">向量相似度阈值,低于此值的文档将被过滤(0-1)</div>
</div>
<div class="mb-3" id="keywordThresholdGroup" style="display: none;">
<label class="form-label">全文检索阈值</label>
<input type="number" class="form-control" id="keywordThreshold" name="keyword_threshold"
value="0.5" step="0.1" min="0" max="1" placeholder="0.5">
<div class="form-text">关键词匹配阈值(0-1)</div>
</div>
<div class="mb-3" id="vectorWeightGroup" style="display: none;">
<label class="form-label">向量检索权重</label>
<input type="number" class="form-control" id="vectorWeight" name="vector_weight"
value="0.7" step="0.1" min="0" max="1" placeholder="0.7">
<div class="form-text">混合检索时向量检索的权重(0-1),关键词检索权重 = 1 - 向量权重</div>
</div>
<div class="mb-3">
<label class="form-label">TopN 结果数量</label>
<input type="number" class="form-control" id="topN" name="top_n"
value="5" min="1" max="50" placeholder="5">
<div class="form-text">返回的文档数量(1-50)</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>向量检索:</strong>基于语义相似度检索,适合理解问题意图</li>
<li><strong>全文检索:</strong>基于关键词匹配检索,适合精确匹配</li>
<li><strong>混合检索:</strong>结合向量和关键词检索,综合两者的优势</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" onclick="resetSettings()">重置</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存设置
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 监听页面加载完成后,执行异步函数
document.addEventListener('DOMContentLoaded', async function() {
try {
// 发送请求获取可用模型列表
const modelsResponse = await fetch('/api/v1/settings/models');
// 解析返回的JSON数据
const modelsResult = await modelsResponse.json();
// 如果请求成功(响应码为200)
if (modelsResult.code === 200) {
// 取得可用模型数据
const availableModels = modelsResult.data;
// 更新嵌入模型表单
updateEmbeddingForm(availableModels);
+ // 更新大语言模型表单
+ updateLLMForm(availableModels)
// 发送请求获取当前设置
const settingsResponse = await fetch('/api/v1/settings');
// 解析返回的JSON数据
const settingsResult = await settingsResponse.json();
// 如果请求成功(响应码为200)
if (settingsResult.code === 200) {
// 加载设置到页面
loadSettings(settingsResult.data);
}
}
} catch (error) {
// 捕获异常并在控制台打印错误信息
console.error('加载设置失败:', error);
// 弹窗提示加载失败
alert('加载设置失败: ' + error.message);
}
});
// 加载设置并填充到表单
function loadSettings(settings) {
// 设置嵌入向量提供商,默认为huggingface
document.getElementById('embeddingProvider').value = settings.embedding_provider || 'huggingface';
+ // 设置大语言模型提供商,默认为deepseek
+ document.getElementById('llmProvider').value = settings.llm_provider || 'deepseek';
// 如果有嵌入模型名称,设置模型名称
if (settings.embedding_model_name) {
document.getElementById('embeddingModelName').value = settings.embedding_model_name;
}
+ if (settings.llm_model_name) {
+ document.getElementById('llmModelName').value = settings.llm_model_name;
+ }
// 设置API Key,如果没有则为空字符串
document.getElementById('embeddingApiKey').value = settings.embedding_api_key || '';
// 设置Base URL,如果没有则为空字符串
document.getElementById('embeddingBaseUrl').value = settings.embedding_base_url || '';
+ // 加载其他 LLM 设置
+ document.getElementById('llmApiKey').value = settings.llm_api_key || '';
+ // 设置Base URL,如果没有则为空字符串
+ document.getElementById('llmBaseUrl').value = settings.llm_base_url || '';
+ // 设置温度,如果没有则为0.7
+ document.getElementById('llmTemperature').value = settings.llm_temperature || '0.7';
}
// 更新嵌入模型表单(根据可用模型动态渲染选项)
function updateEmbeddingForm(availableModels) {
// 获取当前选中的提供商
const provider = document.getElementById('embeddingProvider').value;
// 获取模型选择框
const modelSelect = document.getElementById('embeddingModelName');
// 获取API Key输入框所在元素
const apiKeyGroup = document.getElementById('embeddingApiKeyGroup');
// 获取Base URL输入框所在元素
const baseUrlGroup = document.getElementById('embeddingBaseUrlGroup');
// 记住当前选中的模型值(以便刷新后尽量保留)
const currentValue = modelSelect.value;
// 清空模型下拉框内容,并加一个提示选项
modelSelect.innerHTML = '<option value="">请选择模型</option>';
// 如果存在可用模型数据,并且当前提供商有对应条目
if (availableModels && availableModels.embedding_models[provider]) {
// 获取该提供商的详细信息
const providerInfo = availableModels.embedding_models[provider];
// 遍历所有模型,添加为下拉选项
providerInfo.models.forEach(model => {
// 创建一个option元素
const option = document.createElement('option');
// 选项的值为path属性,否则为name
const optionValue = model.path || model.name;
option.value = optionValue;
// 构造显示文本,包括名称、维度和描述
const displayText = model.name + (model.dimension ? ` (维度: ${model.dimension})` : '') + (model.description ? ' - ' + model.description : '');
option.textContent = displayText;
// 添加到模型下拉框
modelSelect.appendChild(option);
});
// 恢复刷新前选中的值(如果还有)
if (currentValue) {
// 检查当前值是否还在选项中
const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
// 如果存在,直接选中
modelSelect.value = currentValue;
} else {
// 如果不存在,尝试“模糊匹配”
const matchingOption = Array.from(modelSelect.options).find(opt => {
const optValue = opt.value;
// 判断当前值和选项值是否完全相等或相互包含
return optValue === currentValue || optValue.includes(currentValue) || currentValue.includes(optValue);
});
// 如果找到匹配项,则选中该项
if (matchingOption) {
modelSelect.value = matchingOption.value;
}
}
}
// 根据提供商是否需要API Key,显示或隐藏输入框
if (providerInfo.requires_api_key) {
apiKeyGroup.style.display = 'block';
document.getElementById('embeddingApiKey').required = true;
} else {
apiKeyGroup.style.display = 'none';
document.getElementById('embeddingApiKey').required = false;
}
// 根据提供商是否需要Base URL,显示或隐藏输入框
if (providerInfo.requires_base_url) {
baseUrlGroup.style.display = 'block';
} else {
baseUrlGroup.style.display = 'none';
}
}
}
// 保存设置(表单提交处理函数,异步提交到后端)
async function saveSettings(event) {
// 阻止表单的默认提交行为
event.preventDefault();
// 获取表单dom对象
const form = event.target;
// 构造FormData对象用于取值
const formData = new FormData(form);
// 构造要提交的数据对象
const data = {
+ embedding_provider: formData.get('embedding_provider'),// 嵌入模型提供商
+ embedding_model_name: formData.get('embedding_model_name') || null,// 嵌入模型名称
+ embedding_api_key: formData.get('embedding_api_key') || null,// 嵌入API Key
+ embedding_base_url: formData.get('embedding_base_url') || null,// 嵌入Base URL
+ llm_provider: formData.get('llm_provider'),// 大语言模型提供商
+ llm_model_name: formData.get('llm_model_name') || null,// 大语言模型名称
+ llm_api_key: formData.get('llm_api_key') || null,// 大语言API Key
+ llm_base_url: formData.get('llm_base_url') || null,// 大语言Base URL
+ llm_temperature: formData.get('llm_temperature') || null,// 大语言温度
};
try {
// 发送PUT请求到设置API,提交JSON数据
const response = await fetch('/api/v1/settings', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
// 解析返回结果
const result = await response.json();
// 如果提交成功
if (response.ok) {
alert('设置保存成功!');
// 刷新页面
location.reload();
} else {
// 否则弹窗提示失败原因
alert('保存失败: ' + result.message);
}
} catch (error) {
// 捕获异常并弹窗提示
alert('保存失败: ' + error.message);
}
}
+// 定义函数用于更新大语言模型(LLM)表单选项
+function updateLLMForm(availableModels) {
+ // 获取大语言模型提供商的当前选中值
+ const provider = document.getElementById('llmProvider').value;
+ // 获取模型选择下拉框元素
+ const modelSelect = document.getElementById('llmModelName');
+ // 获取 API Key 输入框分组
+ const apiKeyGroup = document.getElementById('llmApiKeyGroup');
+ // 获取 Base URL 输入框分组
+ const baseUrlGroup = document.getElementById('llmBaseUrlGroup');
+ // 在清空模型选项前,保存一下当前选中的选项值
+ const currentValue = modelSelect.value;
+ // 清空模型选择下拉框的所有选项,并添加默认提示选项
+ modelSelect.innerHTML = '<option value="">请选择模型</option>';
+ // 判断可用模型对象及当前 provider 是否存在
+ if (availableModels && availableModels.llm_models[provider]) {
+ // 获取对应 provider 的模型信息
+ const providerInfo = availableModels.llm_models[provider];
+ // 遍历该 provider 的所有模型,依次添加到下拉框
+ providerInfo.models.forEach(model => {
+ // 创建 option 节点
+ const option = document.createElement('option');
+ // 设置 option 的值为模型名
+ option.value = model.name;
+ // 设置 option 的显示文本(包含模型描述)
+ option.textContent = `${model.name}${model.description ? ' - ' + model.description : ''}`;
+ // 添加 option 到下拉框
+ modelSelect.appendChild(option);
+ });
+ // 如果之前选中过某个值,则恢复它(仅限该值仍存在于新选项中时)
+ if (currentValue) {
+ // 检测之前选中的值是否还存在于下拉选项里
+ const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
+ if (optionExists) {
+ // 恢复之前选中的值
+ modelSelect.value = currentValue;
+ }
+ }
+ // 判断该 provider 是否需要 API Key,显示或隐藏 API Key 分组
+ if (providerInfo.requires_api_key) {
+ apiKeyGroup.style.display = 'block';
+ // 设置输入框为必填
+ document.getElementById('llmApiKey').required = true;
+ } else {
+ // 隐藏分组并取消必填
+ apiKeyGroup.style.display = 'none';
+ document.getElementById('llmApiKey').required = false;
+ }
+ // 判断该 provider 是否需要 Base URL,显示或隐藏 Base URL 分组
+ if (providerInfo.requires_base_url) {
+ baseUrlGroup.style.display = 'block';
+ } else {
+ baseUrlGroup.style.display = 'none';
+ }
+ }
+}
</script>
{% endblock %}
6.提示词设置 #
6.1. settings.html #
app/templates/settings.html
{% extends "base.html" %}
{% block title %}设置 - RAG Lite{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">设置</li>
</ol>
</nav>
<h2><i class="bi bi-gear"></i> 系统设置</h2>
<p class="text-muted">配置模型、提示词和检索参数</p>
<form id="settingsForm" onsubmit="saveSettings(event)">
<!-- 标签页导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="embedding-tab" data-bs-toggle="tab" data-bs-target="#embedding" type="button" role="tab">
<i class="bi bi-diagram-3"></i> 向量嵌入模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="llm-tab" data-bs-toggle="tab" data-bs-target="#llm" type="button" role="tab">
<i class="bi bi-robot"></i> 大语言模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab" data-bs-target="#prompt" type="button" role="tab">
<i class="bi bi-chat-quote"></i> 提示词设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="retrieval-tab" data-bs-toggle="tab" data-bs-target="#retrieval" type="button" role="tab">
<i class="bi bi-search"></i> 检索设置
</button>
</li>
</ul>
<!-- 标签页内容 -->
<div class="tab-content" id="settingsTabContent">
<!-- 标签1: 向量嵌入模型 -->
<div class="tab-pane fade show active" id="embedding" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> 向量嵌入模型(Embedding)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="embeddingProvider" name="embedding_provider" onchange="updateEmbeddingForm()" required>
<option value="huggingface">HuggingFace</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择向量嵌入模型提供商</div>
</div>
<div class="mb-3" id="embeddingModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="embeddingModelName" name="embedding_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称或路径</div>
</div>
<div class="mb-3" id="embeddingApiKeyGroup" style="display: none;">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="embeddingApiKey" name="embedding_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="embeddingBaseUrlGroup" style="display: none;">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="embeddingBaseUrl" name="embedding_base_url" placeholder="例如: http://localhost:11434">
<div class="form-text">API Base URL(Ollama 需要)</div>
</div>
</div>
</div>
</div>
<!-- 标签2: 大语言模型 -->
<div class="tab-pane fade" id="llm" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-robot"></i> 大语言模型(LLM)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="llmProvider" name="llm_provider" onchange="updateLLMForm()" required>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择大语言模型提供商</div>
</div>
<div class="mb-3" id="llmModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="llmModelName" name="llm_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称</div>
</div>
<div class="mb-3" id="llmApiKeyGroup">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llmApiKey" name="llm_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="llmBaseUrlGroup">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="llmBaseUrl" name="llm_base_url" placeholder="例如: https://api.deepseek.com">
<div class="form-text">API Base URL</div>
</div>
<div class="mb-3">
<label class="form-label">温度 (Temperature)</label>
<input type="number" class="form-control" id="llmTemperature" name="llm_temperature"
value="0.7" step="0.1" min="0" max="2" placeholder="0.7">
<div class="form-text">控制输出的随机性,值越大越随机(0-2)</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 提示词设置 -->
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- 子标签页导航 -->
<ul class="nav nav-tabs mb-4" id="promptSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="chat-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#chat-prompt-sub" type="button" role="tab">
<i class="bi bi-chat"></i> 普通聊天提示词
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rag-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#rag-prompt-sub" type="button" role="tab">
<i class="bi bi-book"></i> 知识库聊天提示词
</button>
</li>
</ul>
<!-- 子标签页内容 -->
<div class="tab-content" id="promptSubTabContent">
<!-- 子标签1: 普通聊天提示词 -->
<div class="tab-pane fade show active" id="chat-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">普通聊天系统提示词</label>
<textarea class="form-control" id="chatSystemPrompt" name="chat_system_prompt" rows="10"
placeholder="输入普通聊天提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">普通聊天提示词用于指导AI助手在普通聊天(未选择知识库)时的回答风格和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量。</p>
</div>
</div>
</div>
<!-- 子标签2: 知识库聊天提示词 -->
<div class="tab-pane fade" id="rag-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">知识库聊天系统提示词</label>
<textarea class="form-control" id="ragSystemPrompt" name="rag_system_prompt" rows="6"
placeholder="输入知识库聊天系统提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">知识库聊天系统提示词用于在会话开始时设置AI助手的角色和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量(如 {context} 或 {question})。</p>
</div>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">知识库聊天查询提示词</label>
<textarea class="form-control" id="ragQueryPrompt" name="rag_query_prompt" rows="10"
placeholder="例如:文档内容: {context} 问题:{question} 请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。"></textarea>
<div class="form-text mt-2">
<p class="mb-1">知识库聊天查询提示词用于每次提问时构建提示,指导AI助手如何基于文档内容回答问题。</p>
<p class="mb-0"><strong>必须使用以下变量:</strong></p>
<ul class="mb-0">
<li><code>{context}</code> - 检索到的文档内容(必需)</li>
<li><code>{question}</code> - 用户的问题(必需)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 检索设置 -->
<div class="tab-pane fade" id="retrieval" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-search"></i> 检索设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">检索模式 <span class="text-danger">*</span></label>
<select class="form-select" id="retrievalMode" name="retrieval_mode" required>
<option value="vector">向量检索</option>
<option value="keyword">全文检索</option>
<option value="hybrid">混合检索</option>
</select>
<div class="form-text">选择文档检索方式</div>
</div>
<div class="mb-3" id="vectorThresholdGroup">
<label class="form-label">向量检索阈值</label>
<input type="number" class="form-control" id="vectorThreshold" name="vector_threshold"
value="0.2" step="0.1" min="0" max="1" placeholder="0.2">
<div class="form-text">向量相似度阈值,低于此值的文档将被过滤(0-1)</div>
</div>
<div class="mb-3" id="keywordThresholdGroup" style="display: none;">
<label class="form-label">全文检索阈值</label>
<input type="number" class="form-control" id="keywordThreshold" name="keyword_threshold"
value="0.5" step="0.1" min="0" max="1" placeholder="0.5">
<div class="form-text">关键词匹配阈值(0-1)</div>
</div>
<div class="mb-3" id="vectorWeightGroup" style="display: none;">
<label class="form-label">向量检索权重</label>
<input type="number" class="form-control" id="vectorWeight" name="vector_weight"
value="0.7" step="0.1" min="0" max="1" placeholder="0.7">
<div class="form-text">混合检索时向量检索的权重(0-1),关键词检索权重 = 1 - 向量权重</div>
</div>
<div class="mb-3">
<label class="form-label">TopN 结果数量</label>
<input type="number" class="form-control" id="topN" name="top_n"
value="5" min="1" max="50" placeholder="5">
<div class="form-text">返回的文档数量(1-50)</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>向量检索:</strong>基于语义相似度检索,适合理解问题意图</li>
<li><strong>全文检索:</strong>基于关键词匹配检索,适合精确匹配</li>
<li><strong>混合检索:</strong>结合向量和关键词检索,综合两者的优势</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" onclick="resetSettings()">重置</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存设置
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 监听页面加载完成后,执行异步函数
document.addEventListener('DOMContentLoaded', async function() {
try {
// 发送请求获取可用模型列表
const modelsResponse = await fetch('/api/v1/settings/models');
// 解析返回的JSON数据
const modelsResult = await modelsResponse.json();
// 如果请求成功(响应码为200)
if (modelsResult.code === 200) {
// 取得可用模型数据
const availableModels = modelsResult.data;
// 更新嵌入模型表单
updateEmbeddingForm(availableModels);
// 更新大语言模型表单
updateLLMForm(availableModels)
// 发送请求获取当前设置
const settingsResponse = await fetch('/api/v1/settings');
// 解析返回的JSON数据
const settingsResult = await settingsResponse.json();
// 如果请求成功(响应码为200)
if (settingsResult.code === 200) {
// 加载设置到页面
loadSettings(settingsResult.data);
}
}
} catch (error) {
// 捕获异常并在控制台打印错误信息
console.error('加载设置失败:', error);
// 弹窗提示加载失败
alert('加载设置失败: ' + error.message);
}
});
// 加载设置并填充到表单
function loadSettings(settings) {
// 设置嵌入向量提供商,默认为huggingface
document.getElementById('embeddingProvider').value = settings.embedding_provider || 'huggingface';
// 设置大语言模型提供商,默认为deepseek
document.getElementById('llmProvider').value = settings.llm_provider || 'deepseek';
// 如果有嵌入模型名称,设置模型名称
if (settings.embedding_model_name) {
document.getElementById('embeddingModelName').value = settings.embedding_model_name;
}
if (settings.llm_model_name) {
document.getElementById('llmModelName').value = settings.llm_model_name;
}
// 设置API Key,如果没有则为空字符串
document.getElementById('embeddingApiKey').value = settings.embedding_api_key || '';
// 设置Base URL,如果没有则为空字符串
document.getElementById('embeddingBaseUrl').value = settings.embedding_base_url || '';
// 加载其他 LLM 设置
document.getElementById('llmApiKey').value = settings.llm_api_key || '';
// 设置Base URL,如果没有则为空字符串
document.getElementById('llmBaseUrl').value = settings.llm_base_url || '';
// 设置温度,如果没有则为0.7
document.getElementById('llmTemperature').value = settings.llm_temperature || '0.7';
+ // 加载系统提示词
+ document.getElementById('chatSystemPrompt').value = settings.chat_system_prompt || '';
+ // 加载知识库聊天系统提示词
+ document.getElementById('ragSystemPrompt').value = settings.rag_system_prompt || '';
+ // 加载知识库聊天查询提示词
+ document.getElementById('ragQueryPrompt').value = settings.rag_query_prompt || '';
}
// 更新嵌入模型表单(根据可用模型动态渲染选项)
function updateEmbeddingForm(availableModels) {
// 获取当前选中的提供商
const provider = document.getElementById('embeddingProvider').value;
// 获取模型选择框
const modelSelect = document.getElementById('embeddingModelName');
// 获取API Key输入框所在元素
const apiKeyGroup = document.getElementById('embeddingApiKeyGroup');
// 获取Base URL输入框所在元素
const baseUrlGroup = document.getElementById('embeddingBaseUrlGroup');
// 记住当前选中的模型值(以便刷新后尽量保留)
const currentValue = modelSelect.value;
// 清空模型下拉框内容,并加一个提示选项
modelSelect.innerHTML = '<option value="">请选择模型</option>';
// 如果存在可用模型数据,并且当前提供商有对应条目
if (availableModels && availableModels.embedding_models[provider]) {
// 获取该提供商的详细信息
const providerInfo = availableModels.embedding_models[provider];
// 遍历所有模型,添加为下拉选项
providerInfo.models.forEach(model => {
// 创建一个option元素
const option = document.createElement('option');
// 选项的值为path属性,否则为name
const optionValue = model.path || model.name;
option.value = optionValue;
// 构造显示文本,包括名称、维度和描述
const displayText = model.name + (model.dimension ? ` (维度: ${model.dimension})` : '') + (model.description ? ' - ' + model.description : '');
option.textContent = displayText;
// 添加到模型下拉框
modelSelect.appendChild(option);
});
// 恢复刷新前选中的值(如果还有)
if (currentValue) {
// 检查当前值是否还在选项中
const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
// 如果存在,直接选中
modelSelect.value = currentValue;
} else {
// 如果不存在,尝试“模糊匹配”
const matchingOption = Array.from(modelSelect.options).find(opt => {
const optValue = opt.value;
// 判断当前值和选项值是否完全相等或相互包含
return optValue === currentValue || optValue.includes(currentValue) || currentValue.includes(optValue);
});
// 如果找到匹配项,则选中该项
if (matchingOption) {
modelSelect.value = matchingOption.value;
}
}
}
// 根据提供商是否需要API Key,显示或隐藏输入框
if (providerInfo.requires_api_key) {
apiKeyGroup.style.display = 'block';
document.getElementById('embeddingApiKey').required = true;
} else {
apiKeyGroup.style.display = 'none';
document.getElementById('embeddingApiKey').required = false;
}
// 根据提供商是否需要Base URL,显示或隐藏输入框
if (providerInfo.requires_base_url) {
baseUrlGroup.style.display = 'block';
} else {
baseUrlGroup.style.display = 'none';
}
}
}
// 保存设置(表单提交处理函数,异步提交到后端)
async function saveSettings(event) {
// 阻止表单的默认提交行为
event.preventDefault();
// 获取表单dom对象
const form = event.target;
// 构造FormData对象用于取值
const formData = new FormData(form);
// 构造要提交的数据对象
const data = {
embedding_provider: formData.get('embedding_provider'),// 嵌入模型提供商
embedding_model_name: formData.get('embedding_model_name') || null,// 嵌入模型名称
embedding_api_key: formData.get('embedding_api_key') || null,// 嵌入API Key
embedding_base_url: formData.get('embedding_base_url') || null,// 嵌入Base URL
llm_provider: formData.get('llm_provider'),// 大语言模型提供商
llm_model_name: formData.get('llm_model_name') || null,// 大语言模型名称
llm_api_key: formData.get('llm_api_key') || null,// 大语言API Key
llm_base_url: formData.get('llm_base_url') || null,// 大语言Base URL
llm_temperature: formData.get('llm_temperature') || null,// 大语言温度
+ chat_system_prompt: formData.get('chat_system_prompt') || null,// 普通聊天系统提示词
+ rag_system_prompt: formData.get('rag_system_prompt') || null,// 知识库聊天系统提示词
+ rag_query_prompt: formData.get('rag_query_prompt') || null,// 知识库聊天查询提示词
};
try {
// 发送PUT请求到设置API,提交JSON数据
const response = await fetch('/api/v1/settings', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
// 解析返回结果
const result = await response.json();
// 如果提交成功
if (response.ok) {
alert('设置保存成功!');
// 刷新页面
location.reload();
} else {
// 否则弹窗提示失败原因
alert('保存失败: ' + result.message);
}
} catch (error) {
// 捕获异常并弹窗提示
alert('保存失败: ' + error.message);
}
}
// 定义函数用于更新大语言模型(LLM)表单选项
function updateLLMForm(availableModels) {
// 获取大语言模型提供商的当前选中值
const provider = document.getElementById('llmProvider').value;
// 获取模型选择下拉框元素
const modelSelect = document.getElementById('llmModelName');
// 获取 API Key 输入框分组
const apiKeyGroup = document.getElementById('llmApiKeyGroup');
// 获取 Base URL 输入框分组
const baseUrlGroup = document.getElementById('llmBaseUrlGroup');
// 在清空模型选项前,保存一下当前选中的选项值
const currentValue = modelSelect.value;
// 清空模型选择下拉框的所有选项,并添加默认提示选项
modelSelect.innerHTML = '<option value="">请选择模型</option>';
// 判断可用模型对象及当前 provider 是否存在
if (availableModels && availableModels.llm_models[provider]) {
// 获取对应 provider 的模型信息
const providerInfo = availableModels.llm_models[provider];
// 遍历该 provider 的所有模型,依次添加到下拉框
providerInfo.models.forEach(model => {
// 创建 option 节点
const option = document.createElement('option');
// 设置 option 的值为模型名
option.value = model.name;
// 设置 option 的显示文本(包含模型描述)
option.textContent = `${model.name}${model.description ? ' - ' + model.description : ''}`;
// 添加 option 到下拉框
modelSelect.appendChild(option);
});
// 如果之前选中过某个值,则恢复它(仅限该值仍存在于新选项中时)
if (currentValue) {
// 检测之前选中的值是否还存在于下拉选项里
const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
// 恢复之前选中的值
modelSelect.value = currentValue;
}
}
// 判断该 provider 是否需要 API Key,显示或隐藏 API Key 分组
if (providerInfo.requires_api_key) {
apiKeyGroup.style.display = 'block';
// 设置输入框为必填
document.getElementById('llmApiKey').required = true;
} else {
// 隐藏分组并取消必填
apiKeyGroup.style.display = 'none';
document.getElementById('llmApiKey').required = false;
}
// 判断该 provider 是否需要 Base URL,显示或隐藏 Base URL 分组
if (providerInfo.requires_base_url) {
baseUrlGroup.style.display = 'block';
} else {
baseUrlGroup.style.display = 'none';
}
}
}
+function resetSettings() {
+ if (confirm('确定要重置为默认设置吗?')) {
+ loadSettings({
+ embedding_provider: 'huggingface',
+ embedding_model_name: 'sentence-transformers/all-MiniLM-L6-v2',
+ embedding_api_key: '',
+ embedding_base_url: '',
+ llm_provider: 'deepseek',
+ llm_model_name: '',
+ llm_api_key: '',
+ llm_base_url: '',
+ llm_temperature: '0.7',
+ chat_system_prompt: '你是一个专业的AI助手。请友好、准确地回答用户的问题。',
+ rag_system_prompt: '你是一个专业的AI助手。请基于文档内容回答问题。',
+ rag_query_prompt: '文档内容:\n{context}\n\n问题:{question}\n\n请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。',
+ retrieval_mode: 'vector',
+ vector_threshold: '0.2',
+ keyword_threshold: '0.5',
+ vector_weight: '0.7',
+ top_n: '5'
+ });
+ }
+}
</script>
{% endblock %}
7.检索设置 #
7.1. settings.html #
app/templates/settings.html
{% extends "base.html" %}
{% block title %}设置 - RAG Lite{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">设置</li>
</ol>
</nav>
<h2><i class="bi bi-gear"></i> 系统设置</h2>
<p class="text-muted">配置模型、提示词和检索参数</p>
<form id="settingsForm" onsubmit="saveSettings(event)">
<!-- 标签页导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="embedding-tab" data-bs-toggle="tab" data-bs-target="#embedding" type="button" role="tab">
<i class="bi bi-diagram-3"></i> 向量嵌入模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="llm-tab" data-bs-toggle="tab" data-bs-target="#llm" type="button" role="tab">
<i class="bi bi-robot"></i> 大语言模型
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab" data-bs-target="#prompt" type="button" role="tab">
<i class="bi bi-chat-quote"></i> 提示词设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="retrieval-tab" data-bs-toggle="tab" data-bs-target="#retrieval" type="button" role="tab">
<i class="bi bi-search"></i> 检索设置
</button>
</li>
</ul>
<!-- 标签页内容 -->
<div class="tab-content" id="settingsTabContent">
<!-- 标签1: 向量嵌入模型 -->
<div class="tab-pane fade show active" id="embedding" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> 向量嵌入模型(Embedding)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="embeddingProvider" name="embedding_provider" onchange="updateEmbeddingForm()" required>
<option value="huggingface">HuggingFace</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择向量嵌入模型提供商</div>
</div>
<div class="mb-3" id="embeddingModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="embeddingModelName" name="embedding_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称或路径</div>
</div>
<div class="mb-3" id="embeddingApiKeyGroup" style="display: none;">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="embeddingApiKey" name="embedding_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="embeddingBaseUrlGroup" style="display: none;">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="embeddingBaseUrl" name="embedding_base_url" placeholder="例如: http://localhost:11434">
<div class="form-text">API Base URL(Ollama 需要)</div>
</div>
</div>
</div>
</div>
<!-- 标签2: 大语言模型 -->
<div class="tab-pane fade" id="llm" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-robot"></i> 大语言模型(LLM)</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">提供商 <span class="text-danger">*</span></label>
<select class="form-select" id="llmProvider" name="llm_provider" onchange="updateLLMForm()" required>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">选择大语言模型提供商</div>
</div>
<div class="mb-3" id="llmModelNameGroup">
<label class="form-label">模型名称</label>
<select class="form-select" id="llmModelName" name="llm_model_name">
<option value="">请选择模型</option>
<!-- 动态填充 -->
</select>
<div class="form-text">选择模型名称</div>
</div>
<div class="mb-3" id="llmApiKeyGroup">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llmApiKey" name="llm_api_key" placeholder="输入 API Key">
<div class="form-text">某些提供商需要 API Key</div>
</div>
<div class="mb-3" id="llmBaseUrlGroup">
<label class="form-label">Base URL</label>
<input type="text" class="form-control" id="llmBaseUrl" name="llm_base_url" placeholder="例如: https://api.deepseek.com">
<div class="form-text">API Base URL</div>
</div>
<div class="mb-3">
<label class="form-label">温度 (Temperature)</label>
<input type="number" class="form-control" id="llmTemperature" name="llm_temperature"
value="0.7" step="0.1" min="0" max="2" placeholder="0.7">
<div class="form-text">控制输出的随机性,值越大越随机(0-2)</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 提示词设置 -->
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- 子标签页导航 -->
<ul class="nav nav-tabs mb-4" id="promptSubTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="chat-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#chat-prompt-sub" type="button" role="tab">
<i class="bi bi-chat"></i> 普通聊天提示词
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rag-prompt-sub-tab" data-bs-toggle="tab" data-bs-target="#rag-prompt-sub" type="button" role="tab">
<i class="bi bi-book"></i> 知识库聊天提示词
</button>
</li>
</ul>
<!-- 子标签页内容 -->
<div class="tab-content" id="promptSubTabContent">
<!-- 子标签1: 普通聊天提示词 -->
<div class="tab-pane fade show active" id="chat-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">普通聊天系统提示词</label>
<textarea class="form-control" id="chatSystemPrompt" name="chat_system_prompt" rows="10"
placeholder="输入普通聊天提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">普通聊天提示词用于指导AI助手在普通聊天(未选择知识库)时的回答风格和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量。</p>
</div>
</div>
</div>
<!-- 子标签2: 知识库聊天提示词 -->
<div class="tab-pane fade" id="rag-prompt-sub" role="tabpanel">
<div class="mb-4">
<label class="form-label">知识库聊天系统提示词</label>
<textarea class="form-control" id="ragSystemPrompt" name="rag_system_prompt" rows="6"
placeholder="输入知识库聊天系统提示词..."></textarea>
<div class="form-text mt-2">
<p class="mb-0">知识库聊天系统提示词用于在会话开始时设置AI助手的角色和行为。</p>
<p class="mb-0 mt-2"><strong>注意:</strong>这是系统消息的内容,不能使用变量(如 {context} 或 {question})。</p>
</div>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">知识库聊天查询提示词</label>
<textarea class="form-control" id="ragQueryPrompt" name="rag_query_prompt" rows="10"
placeholder="例如:文档内容: {context} 问题:{question} 请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。"></textarea>
<div class="form-text mt-2">
<p class="mb-1">知识库聊天查询提示词用于每次提问时构建提示,指导AI助手如何基于文档内容回答问题。</p>
<p class="mb-0"><strong>必须使用以下变量:</strong></p>
<ul class="mb-0">
<li><code>{context}</code> - 检索到的文档内容(必需)</li>
<li><code>{question}</code> - 用户的问题(必需)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 标签3: 检索设置 -->
<div class="tab-pane fade" id="retrieval" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-search"></i> 检索设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">检索模式 <span class="text-danger">*</span></label>
<select class="form-select" id="retrievalMode" name="retrieval_mode" required>
<option value="vector">向量检索</option>
<option value="keyword">全文检索</option>
<option value="hybrid">混合检索</option>
</select>
<div class="form-text">选择文档检索方式</div>
</div>
<div class="mb-3" id="vectorThresholdGroup">
<label class="form-label">向量检索阈值</label>
<input type="number" class="form-control" id="vectorThreshold" name="vector_threshold"
value="0.2" step="0.1" min="0" max="1" placeholder="0.2">
<div class="form-text">向量相似度阈值,低于此值的文档将被过滤(0-1)</div>
</div>
<div class="mb-3" id="keywordThresholdGroup" style="display: none;">
<label class="form-label">全文检索阈值</label>
<input type="number" class="form-control" id="keywordThreshold" name="keyword_threshold"
value="0.5" step="0.1" min="0" max="1" placeholder="0.5">
<div class="form-text">关键词匹配阈值(0-1)</div>
</div>
<div class="mb-3" id="vectorWeightGroup" style="display: none;">
<label class="form-label">向量检索权重</label>
<input type="number" class="form-control" id="vectorWeight" name="vector_weight"
value="0.7" step="0.1" min="0" max="1" placeholder="0.7">
<div class="form-text">混合检索时向量检索的权重(0-1),关键词检索权重 = 1 - 向量权重</div>
</div>
<div class="mb-3">
<label class="form-label">TopN 结果数量</label>
<input type="number" class="form-control" id="topN" name="top_n"
value="5" min="1" max="50" placeholder="5">
<div class="form-text">返回的文档数量(1-50)</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>向量检索:</strong>基于语义相似度检索,适合理解问题意图</li>
<li><strong>全文检索:</strong>基于关键词匹配检索,适合精确匹配</li>
<li><strong>混合检索:</strong>结合向量和关键词检索,综合两者的优势</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" onclick="resetSettings()">重置</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存设置
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 监听页面加载完成后,执行异步函数
document.addEventListener('DOMContentLoaded', async function() {
try {
// 发送请求获取可用模型列表
const modelsResponse = await fetch('/api/v1/settings/models');
// 解析返回的JSON数据
const modelsResult = await modelsResponse.json();
// 如果请求成功(响应码为200)
if (modelsResult.code === 200) {
// 取得可用模型数据
const availableModels = modelsResult.data;
// 更新嵌入模型表单
updateEmbeddingForm(availableModels);
// 更新大语言模型表单
updateLLMForm(availableModels)
+ // 更新检索设置表单
+ updateRetrievalForm();
// 发送请求获取当前设置
const settingsResponse = await fetch('/api/v1/settings');
// 解析返回的JSON数据
const settingsResult = await settingsResponse.json();
// 如果请求成功(响应码为200)
if (settingsResult.code === 200) {
// 加载设置到页面
loadSettings(settingsResult.data);
}
}
} catch (error) {
// 捕获异常并在控制台打印错误信息
console.error('加载设置失败:', error);
// 弹窗提示加载失败
alert('加载设置失败: ' + error.message);
}
});
+function updateRetrievalForm() {
+ const mode = document.getElementById('retrievalMode').value;
+ const vectorThresholdGroup = document.getElementById('vectorThresholdGroup');
+ const keywordThresholdGroup = document.getElementById('keywordThresholdGroup');
+ const vectorWeightGroup = document.getElementById('vectorWeightGroup');
+ if (mode === 'vector') {
+ vectorThresholdGroup.style.display = 'block';
+ keywordThresholdGroup.style.display = 'none';
+ vectorWeightGroup.style.display = 'none';
+ } else if (mode === 'keyword') {
+ vectorThresholdGroup.style.display = 'none';
+ keywordThresholdGroup.style.display = 'block';
+ vectorWeightGroup.style.display = 'none';
+ } else if (mode === 'hybrid') {
+ vectorThresholdGroup.style.display = 'block';
+ keywordThresholdGroup.style.display = 'block';
+ vectorWeightGroup.style.display = 'block';
+ }
+}
// 加载设置并填充到表单
function loadSettings(settings) {
// 设置嵌入向量提供商,默认为huggingface
document.getElementById('embeddingProvider').value = settings.embedding_provider || 'huggingface';
// 设置大语言模型提供商,默认为deepseek
document.getElementById('llmProvider').value = settings.llm_provider || 'deepseek';
// 如果有嵌入模型名称,设置模型名称
if (settings.embedding_model_name) {
document.getElementById('embeddingModelName').value = settings.embedding_model_name;
}
if (settings.llm_model_name) {
document.getElementById('llmModelName').value = settings.llm_model_name;
}
// 设置API Key,如果没有则为空字符串
document.getElementById('embeddingApiKey').value = settings.embedding_api_key || '';
// 设置Base URL,如果没有则为空字符串
document.getElementById('embeddingBaseUrl').value = settings.embedding_base_url || '';
// 加载其他 LLM 设置
document.getElementById('llmApiKey').value = settings.llm_api_key || '';
// 设置Base URL,如果没有则为空字符串
document.getElementById('llmBaseUrl').value = settings.llm_base_url || '';
// 设置温度,如果没有则为0.7
document.getElementById('llmTemperature').value = settings.llm_temperature || '0.7';
// 加载系统提示词
document.getElementById('chatSystemPrompt').value = settings.chat_system_prompt || '';
// 加载知识库聊天系统提示词
document.getElementById('ragSystemPrompt').value = settings.rag_system_prompt || '';
// 加载知识库聊天查询提示词
document.getElementById('ragQueryPrompt').value = settings.rag_query_prompt || '';
+ // 设置检索模式,默认为'vector'
+ document.getElementById('retrievalMode').value = settings.retrieval_mode || 'vector';
+ // 设置向量检索阈值,默认为'0.2'
+ document.getElementById('vectorThreshold').value = settings.vector_threshold || '0.2';
+ // 设置关键词检索阈值,默认为'0.5'
+ document.getElementById('keywordThreshold').value = settings.keyword_threshold || '0.5';
+ // 设置向量权重,默认为'0.7'
+ document.getElementById('vectorWeight').value = settings.vector_weight || '0.7';
+ // 设置topN,默认为'5'
+ document.getElementById('topN').value = settings.top_n || '5';
}
// 更新嵌入模型表单(根据可用模型动态渲染选项)
function updateEmbeddingForm(availableModels) {
// 获取当前选中的提供商
const provider = document.getElementById('embeddingProvider').value;
// 获取模型选择框
const modelSelect = document.getElementById('embeddingModelName');
// 获取API Key输入框所在元素
const apiKeyGroup = document.getElementById('embeddingApiKeyGroup');
// 获取Base URL输入框所在元素
const baseUrlGroup = document.getElementById('embeddingBaseUrlGroup');
// 记住当前选中的模型值(以便刷新后尽量保留)
const currentValue = modelSelect.value;
// 清空模型下拉框内容,并加一个提示选项
modelSelect.innerHTML = '<option value="">请选择模型</option>';
// 如果存在可用模型数据,并且当前提供商有对应条目
if (availableModels && availableModels.embedding_models[provider]) {
// 获取该提供商的详细信息
const providerInfo = availableModels.embedding_models[provider];
// 遍历所有模型,添加为下拉选项
providerInfo.models.forEach(model => {
// 创建一个option元素
const option = document.createElement('option');
// 选项的值为path属性,否则为name
const optionValue = model.path || model.name;
option.value = optionValue;
// 构造显示文本,包括名称、维度和描述
const displayText = model.name + (model.dimension ? ` (维度: ${model.dimension})` : '') + (model.description ? ' - ' + model.description : '');
option.textContent = displayText;
// 添加到模型下拉框
modelSelect.appendChild(option);
});
// 恢复刷新前选中的值(如果还有)
if (currentValue) {
// 检查当前值是否还在选项中
const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
// 如果存在,直接选中
modelSelect.value = currentValue;
} else {
// 如果不存在,尝试“模糊匹配”
const matchingOption = Array.from(modelSelect.options).find(opt => {
const optValue = opt.value;
// 判断当前值和选项值是否完全相等或相互包含
return optValue === currentValue || optValue.includes(currentValue) || currentValue.includes(optValue);
});
// 如果找到匹配项,则选中该项
if (matchingOption) {
modelSelect.value = matchingOption.value;
}
}
}
// 根据提供商是否需要API Key,显示或隐藏输入框
if (providerInfo.requires_api_key) {
apiKeyGroup.style.display = 'block';
document.getElementById('embeddingApiKey').required = true;
} else {
apiKeyGroup.style.display = 'none';
document.getElementById('embeddingApiKey').required = false;
}
// 根据提供商是否需要Base URL,显示或隐藏输入框
if (providerInfo.requires_base_url) {
baseUrlGroup.style.display = 'block';
} else {
baseUrlGroup.style.display = 'none';
}
}
}
// 保存设置(表单提交处理函数,异步提交到后端)
async function saveSettings(event) {
// 阻止表单的默认提交行为
event.preventDefault();
// 获取表单dom对象
const form = event.target;
// 构造FormData对象用于取值
const formData = new FormData(form);
// 构造要提交的数据对象
const data = {
embedding_provider: formData.get('embedding_provider'),// 嵌入模型提供商
embedding_model_name: formData.get('embedding_model_name') || null,// 嵌入模型名称
embedding_api_key: formData.get('embedding_api_key') || null,// 嵌入API Key
embedding_base_url: formData.get('embedding_base_url') || null,// 嵌入Base URL
llm_provider: formData.get('llm_provider'),// 大语言模型提供商
llm_model_name: formData.get('llm_model_name') || null,// 大语言模型名称
llm_api_key: formData.get('llm_api_key') || null,// 大语言API Key
llm_base_url: formData.get('llm_base_url') || null,// 大语言Base URL
llm_temperature: formData.get('llm_temperature') || null,// 大语言温度
chat_system_prompt: formData.get('chat_system_prompt') || null,// 普通聊天系统提示词
rag_system_prompt: formData.get('rag_system_prompt') || null,// 知识库聊天系统提示词
rag_query_prompt: formData.get('rag_query_prompt') || null,// 知识库聊天查询提示词
+ retrieval_mode: formData.get('retrieval_mode'),// 检索模式
+ vector_threshold: formData.get('vector_threshold') || null,// 向量检索阈值
+ keyword_threshold: formData.get('keyword_threshold') || null,// 全文检索阈值
+ vector_weight: formData.get('vector_weight') || null,// 向量检索权重
+ top_n: formData.get('top_n') || null// TopN 结果数量
};
try {
// 发送PUT请求到设置API,提交JSON数据
const response = await fetch('/api/v1/settings', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
// 解析返回结果
const result = await response.json();
// 如果提交成功
if (response.ok) {
alert('设置保存成功!');
// 刷新页面
location.reload();
} else {
// 否则弹窗提示失败原因
alert('保存失败: ' + result.message);
}
} catch (error) {
// 捕获异常并弹窗提示
alert('保存失败: ' + error.message);
}
}
// 定义函数用于更新大语言模型(LLM)表单选项
function updateLLMForm(availableModels) {
// 获取大语言模型提供商的当前选中值
const provider = document.getElementById('llmProvider').value;
// 获取模型选择下拉框元素
const modelSelect = document.getElementById('llmModelName');
// 获取 API Key 输入框分组
const apiKeyGroup = document.getElementById('llmApiKeyGroup');
// 获取 Base URL 输入框分组
const baseUrlGroup = document.getElementById('llmBaseUrlGroup');
// 在清空模型选项前,保存一下当前选中的选项值
const currentValue = modelSelect.value;
// 清空模型选择下拉框的所有选项,并添加默认提示选项
modelSelect.innerHTML = '<option value="">请选择模型</option>';
// 判断可用模型对象及当前 provider 是否存在
if (availableModels && availableModels.llm_models[provider]) {
// 获取对应 provider 的模型信息
const providerInfo = availableModels.llm_models[provider];
// 遍历该 provider 的所有模型,依次添加到下拉框
providerInfo.models.forEach(model => {
// 创建 option 节点
const option = document.createElement('option');
// 设置 option 的值为模型名
option.value = model.name;
// 设置 option 的显示文本(包含模型描述)
option.textContent = `${model.name}${model.description ? ' - ' + model.description : ''}`;
// 添加 option 到下拉框
modelSelect.appendChild(option);
});
// 如果之前选中过某个值,则恢复它(仅限该值仍存在于新选项中时)
if (currentValue) {
// 检测之前选中的值是否还存在于下拉选项里
const optionExists = Array.from(modelSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
// 恢复之前选中的值
modelSelect.value = currentValue;
}
}
// 判断该 provider 是否需要 API Key,显示或隐藏 API Key 分组
if (providerInfo.requires_api_key) {
apiKeyGroup.style.display = 'block';
// 设置输入框为必填
document.getElementById('llmApiKey').required = true;
} else {
// 隐藏分组并取消必填
apiKeyGroup.style.display = 'none';
document.getElementById('llmApiKey').required = false;
}
// 判断该 provider 是否需要 Base URL,显示或隐藏 Base URL 分组
if (providerInfo.requires_base_url) {
baseUrlGroup.style.display = 'block';
} else {
baseUrlGroup.style.display = 'none';
}
}
}
function resetSettings() {
if (confirm('确定要重置为默认设置吗?')) {
loadSettings({
embedding_provider: 'huggingface',
embedding_model_name: 'sentence-transformers/all-MiniLM-L6-v2',
embedding_api_key: '',
embedding_base_url: '',
llm_provider: 'deepseek',
llm_model_name: '',
llm_api_key: '',
llm_base_url: '',
llm_temperature: '0.7',
chat_system_prompt: '你是一个专业的AI助手。请友好、准确地回答用户的问题。',
rag_system_prompt: '你是一个专业的AI助手。请基于文档内容回答问题。',
rag_query_prompt: '文档内容:\n{context}\n\n问题:{question}\n\n请基于文档内容回答问题。如果文档中没有相关信息,请明确说明。',
retrieval_mode: 'vector',
vector_threshold: '0.2',
keyword_threshold: '0.5',
vector_weight: '0.7',
top_n: '5'
});
}
}
</script>
{% endblock %}