Base/Models 层详解------小白也能看懂
📍 原文位置:
D:\AI0409\Day22\Models-学习指南.md本文档是这个包的系统学习指南,目标:让完全不懂数据库的小白也能理解 Models 层是什么、怎么用、为什么这样设计。
前置知识:在看这个之前,你需要知道......
🗄️ 数据库是什么
数据库 = 数据的仓库。就像 Excel 表格,但这个"表格"不是给人看的,是给程序看的。
Excel 表格(给人看): 数据库表(给程序看):
┌──────┬──────┐ ┌──────┬──────┐
│ 姓名 │ 年龄 │ │ name │ age │
├──────┼──────┤ ├──────┼──────┤
│ 张三 │ 25 │ │"张三" │ 25 │
│ 李四 │ 30 │ │"李四" │ 30 │
└──────┴──────┘ └──────┴──────┘
🔢 两种完全不同的"查东西"方式
普通查询 :SELECT * FROM users WHERE name = "张三"
→ 精确匹配,张三就是张三,不认识"姓张的人"。
向量查询 :找出和"张三"最像的名字
→ 语义理解,李四、王五、赵六......都能找到,甚至英文名"John"也能找到。
类比:
| 查询方式 | 比喻 |
|---|---|
| 普通查询(WHERE) | 📇 查字典------"张三"就只找"张三"这个词 |
| 向量查询(语义搜索) | 🧠 问人------"谁和孙悟空最像?"你会想到猪八戒、沙僧,都对 |
📐 整体定位:Models 层在系统中站在哪儿
先搞清楚一件事------Models 在整个系统里是什么角色。
想象你要盖一栋楼:
完整系统架构(分层):
┌─────────────────────────────┐
│ 用户 / 前端 / App │ ← 最外层,用户能看到的
└────────────┬────────────────┘
▼ 发送请求
┌─────────────────────────────┐
│ Base/Api(接口层) │ ← 接收请求,决定交给谁处理
└────────────┬────────────────┘
▼ 使用
┌─────────────────────────────┐
│ Base/Models(数据模型层) │ ← ★ 本文档讲的就是这里
│ "数据长什么样" │
│ "有哪些查询方法" │
└────────────┬────────────────┘
▼ 继承
┌─────────────────────────────┐
│ Base/Repository(数据库层) │ ← 真正执行 SQL 的地方
│ "SQL 怎么写" │
│ "数据库怎么连" │
└─────────────────────────────┘
Models 层就是"数据模具":告诉程序"我这张表有几个字段、每个字段是什么类型、可以做什么查询"。
第1步:Models 层的两种模型------这是理解一切的钥匙
🔑 核心概念:两种数据库,两种模型
Models 层包含两类模型:
┌──────────────────────────────┐
│ 关系型模型(MySQL) │
│ │
│ 继承:BaseModuleDBModel │
│ 特点:精确查询(WHERE column = 值)│
│ 例子:找到 session_id="abc" 的会话│
└──────────────────────────────┘
┌──────────────────────────────┐
│ 向量模型(Milvus) │
│ │
│ 继承:BaseVDBModel │
│ 特点:语义搜索(找最相似的) │
│ 例子:找到和"电池续航"最像的词 │
└──────────────────────────────┘
🏠 生活类比
MySQL(关系型) = 图书馆的索书号系统:
- 每个书架都有编号(A区-1号架-B面-第3层)
- 想要《西游记》?记住编号
A-1-B-3,按编号去找,一定能找到 - 精确、稳定,但只知道"编号=什么"
Milvus(向量型) = 图书馆的智能推荐系统:
- 你说"我喜欢看西游记"
- 系统自动推荐:《三国演义》《水浒传》《红楼梦》......
- 它找的不是"名字一样",是"味道差不多"
- 语义理解,但有一点点模糊性
第2步:关系型模型详解(MySQL)
2.1 标准结构------所有模型都长这样
每个模型都遵循"四件套"格式:
python
class MyModel(BaseModuleDBModel):
# 第1件:表名(告诉程序这个模型对应哪张表)
table_alias: ClassVar[str] = "my_table"
# 第2件:建表 SQL(如果表不存在,框架自动按这个建)
create_table_sql: ClassVar[str] = f"""CREATE TABLE `{table_alias}` (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
age INT,
...
)"""
# 第3件:字段定义(Pydantic 的 Field,每个字段对应一列)
id: Optional[int] = Field(None, description="主键ID")
name: str = Field(..., description="名称") # ... = 必填
age: int = Field(18, description="年龄") # 18 = 默认值
# 第4件:自定义查询方法(按业务需要添加)
@classmethod
def get_by_name(cls, name: str):
"""按名字查找"""
results = cls.find_by(name=name, limit=1)
return results[0] if results else None
为什么要用 ClassVar?
python
# 普通字段 = 每个实例有一份
# ClassVar 字段 = 整个类只有一份(不实例化也有的字段)
class Student:
name: str = "李四" # 每次创建学生对象,这个名字都是"李四"
school: ClassVar[str] = "宜春学院" # 所有学生共享一个学校名
# 测试
s1 = Student()
s2 = Student()
s1.name = "张三"
print(s1.name) # "张三"
print(s2.name) # "李四"(不变)
print(Student.school) # "宜春学院"(所有学生共享)
table_alias 和 create_table_sql 必须是 ClassVar,因为它们描述的是"表"的共同属性,而不是"某一行数据"的属性。
2.2 BaseLLMSession ------ 用户会话模型
类比:微信里的"聊天窗口"。
你在微信里打开一个新聊天 → 就是新建一个 Session
这个聊天窗口里有:
- 窗口的唯一ID(session_uuid)
- 这个窗口属于哪个用户(user_id)
- 窗口的标题(title)
- 用的是哪个 AI 模型(model_name)
- AI 自动总结的对话摘要(ai_summary)
- 内容标签(tags)
- 上次看到哪条消息了(last_handle_id)
- 创建时间 / 更新时间
字段详解:
python
session_uuid: str # "abc-123-def..." → 窗口的唯一身份证号
user_id: str # "user_001" → 属于哪个用户
title: str # "新会话" → 窗口标题(用户可改)
model_name: str # "qwen-plus" → 用哪个 AI 模型回答
ai_summary: str # "用户问了Python的问题" → AI 自动写的摘要
tags: str # "编程,Python" → 内容标签
last_handle_id: int # 42 → 上次处理到第42条消息
created_at: datetime # 2026-01-01 12:00 → 窗口什么时候创建的
update_at: datetime # 2026-01-01 14:00 → 最后一次有消息的时间
核心方法图解:
┌─────────────────────────────────────────────────┐
│ get_or_create_session(user_id) │
│ │
│ 输入:user_id = "user_001" │
│ │
│ 逻辑: │
│ 这个用户有会话? │
│ ├─ 有 → 返回已有会话 │
│ └─ 没有 → 创建新会话,返回 │
│ │
│ 用途:用户发消息时,确保有一个地方可以存放消息 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ get_user_last_session(user_id) │
│ │
│ 智能容错逻辑: │
│ │
│ Step1: 传了 session_id 吗? │
│ └─ 传了 → 找这个 ID │
│ └─ 找到了 → 返回 │
│ └─ 没找到 → 继续 Step2 │
│ └─ 没传 或 没找到 → 继续 Step2 │
│ │
│ Step2: 找该用户最近一次会话 │
│ └─ 找到了 → 返回 │
│ └─ 也没有 → 创建新会话,返回 │
│ │
│ 用途:容错------用户填错了 session_id 也尽量给结果 │
└─────────────────────────────────────────────────┘
2.3 BaseLLMConversationModel ------ 对话记录模型
类比:聊天窗口里的"每一条消息"。
一个微信聊天窗口(Session)
│
├── 第1条消息(Conversation): 用户:"今天天气如何?" AI:"晴天,25度"
├── 第2条消息(Conversation): 用户:"适合出门吗?" AI:"适合,注意防晒"
├── 第3条消息(Conversation): 用户:"推荐个旅游景点?" AI:"建议去西湖..."
└── 第4条消息(Conversation): ...
字段详解:
python
id: int # 这条对话记录的身份证号
session_id: str # 属于哪个会话窗口
user_id: str # 哪个用户问的
question: str # 用户原始问题
rewrite_question: str # AI 改写后的问题(更清晰、更规范)
answer: str # AI 的回答
context: str # 上下文(前几轮对话内容,拼在一起)
ai_model: str # 用的是哪个 AI 模型
stream_mode: str # "0"=普通回答 "1"=流式输出 "2"=带思考过程
status: str # "success"=成功 "failed"=失败 "timeout"=超时
error_msg: str # 如果失败了,存什么错
source: str # 从哪个接口来的("base_chat_api" 等)
duration_ms: int # 回答用了多少毫秒(2500 = 2.5秒)
created_at: datetime # 什么时候问的
特别设计:思考模式(stream_mode=2)
当 AI 需要"先想再答"时:
python
# 数据库存的 answer 字段实际上存的是这样一段 JSON:
answer_raw = """
{
"reasoning": "让我分析一下:用户问的是特斯拉续航问题...",
"content": "特斯拉 Model Y 标准续航约 525km,长续航版约 550km"
}
"""
# 普通方式取答案:
answer = conversation.answer # 返回完整 JSON,包含推理过程
# 安全方式取答案(推荐):
answer = conversation.get_answer # 自动解析 JSON,只返回"content"部分
为什么这样设计?
- 推理过程(reasoning)太长、太啰嗦,展示给用户会吓跑人
get_answer属性自动把推理过程藏起来,只显示最终答案- 但数据库里保留了完整信息------如果要审计或调试,还能查到
2.4 BaseKeywordModel ------ 关键词模型
类比:专业的业务术语词典。
比如"续航"这个词:
标准名称: 电池续航能力 ← 正式叫法
编码: BATTERY_RANGE ← 程序里用这个(不会写错字)
同义词: 续航,电池,电量,里程 ← 用户可能用这些词
语义描述: 电动车一次充电能跑多远 ← AI 理解用的解释
状态: 1(可用) ← 还没审核的不能参与搜索
字段详解:
python
keyword_name: str # "电池续航能力" ← 标准名称(数据库存的)
keyword_code: str # "BATTERY_RANGE" ← 英文编码(程序用这个,不会写错)
keyword_desc: str # "车辆一次充电的行驶里程" ← 给人的解释
semantic_desc: str # "电动车续航相关概念" ← 给 AI 看的解释
keyword_synonyms: str # "续航,电池,电量,里程" ← 这些词都应该映射到标准名称
status: int # 1=可用 2=可用但不能同步向量库 0=禁用
type: str # "TECH"(技术类)/ "POLICY"(政策类)等
source: str # "MANUAL"(人工)/ "AI"(AI生成)/ "IMPORT"(批量导入)
为什么关键词要分三种来源?
MANUAL(人工):人工审核过的,绝对准确,但建库慢
AI(AI生成):AI 自动提取的,速度快,但可能有错误,需要人工复核
IMPORT(批量导入):从外部系统批量导进来的,比如从监管文件批量生成
Workflow:
监管文件 → 批量 IMPORT → AI 生成候选关键词 → 人工审核(MANUAL)→ 上线
2.5 BaseParamsModel ------ 系统参数模型
类比:手机里的"设置"菜单。
手机设置:
Wi-Fi 设置 → 开/关
通知设置 → 开/关
亮度设置 → 1~100 的数字
系统参数:
邮件发件人地址 → 配置好的邮箱地址
AI 模型选择 → qwen-plus / deepseek-chat
Session 超时时间 → 3600 秒
字段详解:
python
code: str # "email_sender" ← 程序查询时用的唯一标识
value: str # "noreply@xx.com" ← 参数的实际值(存成字符串)
desc: str # "发件人邮箱地址" ← 给管理员看的说明
parent_code: str # "email_config" ← 属于哪个分组
type: str # "Email" ← 参数的类型/分组
status: int # 1=启用 0=禁用
sort_order: int # 1 ← 界面展示时的排序
特殊设计:LRU 缓存
参数是"读多写少"的典型场景:
python
# 没有缓存时:
# 用户每问一次 → 查数据库一次 → 数据库压力很大
# 有 LRU 缓存后:
@lru_cache(maxsize=128)
def _cached_get_param(code):
return db.query(code) # 第1次调用:查数据库,把结果记住
# 第2次调用(同样的code):直接返回记住的结果,不查数据库!
# 第3次、第4次......都是直接返回!
# 最大记住 128 个参数
# 如果查了 129 个不同的参数,最早记住的那个就被踢出去
缓存什么时候会失效?
python
# 管理员改了一个参数的值
BaseParamsModel.update_by_code("email_sender", "new@email.com")
# 缓存还是旧的!
# → 必须手动清除缓存
BaseParamsModel.clear_param_cache()
# 清除后,下次查询会重新从数据库读,读到新值
2.6 BaseEmailModels ------ 邮件记录模型
类比:邮件系统的"已发送"文件夹。
你发了一封邮件 → 存在"已发送"里 → 记录着:
- 谁发的
- 发给谁
- 什么时候发的
- 发送是否成功
- 如果失败了,重试了几次
字段详解:
python
sender_email: str # "noreply@company.com"
receiver_emails: str # "user1@xx,user2@yy,user3@zz" ← 多人用逗号分隔
email_type: str # "system"(系统邮件)/ "normal"(普通邮件)
subject: str # 邮件标题
body: str # 邮件正文(HTML格式)
is_html: bool # True=HTML格式 False=纯文本
status: str # "pending"(等待发送)/ "success"(成功)/ "failed"(失败)
retry_count: int # 失败了重试了几次
priority: int # 优先级(数值大的先发)
created_at: datetime # 什么时候创建的
为什么邮件也要记录?
- 发失败了 → 用户投诉"没收到" → 查邮件记录,确认是否发送过、失败原因
- 审计需求 → 金融系统要求记录所有发出的通知
- 重试机制 → 失败了可以按 retry_count 决定要不要再发
第3步:向量模型详解(Milvus)
3.1 普通查询 vs 向量查询------对比理解
场景:用户问"特斯拉的电池能用多久?"
普通查询(WHERE):精准匹配
SELECT * FROM conversations WHERE question = "特斯拉的电池能用多久?"
→ 只有完全一样的句子才找到
→ 用户换个说法就查不到 ❌
向量查询(语义搜索):意思相近就算匹配
把问题转成向量 → 找"最相似的"历史对话
→ "特斯拉续航如何?" ✅ 找到
→ "Model Y 充一次电能跑多远?" ✅ 找到
→ "电池寿命多久?" ✅ 找到
3.2 向量是什么
把文字变成一串数字:
"特斯拉续航如何" → [0.72, 0.15, 0.88, 0.34, ...] ← 1024维向量
"Model Y 跑多远" → [0.70, 0.18, 0.85, 0.30, ...] ← 1024维向量
这两句话意思相近 → 数字也很接近 ✅
(0.72 vs 0.70,0.15 vs 0.18......)
"今天天气不错" → [0.05, 0.91, 0.22, 0.11, ...] ← 1024维向量
话题不同 → 数字差距很大 ❌
3.3 VdbLLMConversation ------ 对话向量模型
作用:把历史对话存成向量,支持语义检索。
当用户问了一个新问题:
"特斯拉电池续航怎么样?"
1. 把这个问题转成向量:Q_vector = [0.72, 0.15, ...]
2. 在 Milvus 里找向量最接近的历史对话:
┌──────────────────────────────────────────┐
│ 历史对话向量库 │
│ │
│ [0.72, 0.15, 0.88, ...] → "特斯拉续航约550km"│ ← 最接近!
│ [0.68, 0.20, 0.80, ...] → "Model Y 续航强" │
│ [0.30, 0.85, 0.40, ...] → "今天天气真好" │ ← 差距大
│ │
└──────────────────────────────────────────┘
3. 返回最相似的几条历史对话 → 作为上下文传给 AI
→ AI 基于这些上下文回答 → 更精准、更有依据
字段详解:
python
class VdbLLMConversation(BaseVDBModel):
collection_alias = "llm_conversation" # Milvus 里这个集合的名字
# 标量字段(普通字段,存具体值)
db_id: str # 对应 MySQL 里这条记录的 id
session_id: str # 属于哪个会话
user_id: str # 属于哪个用户
question: str # 用户问题
rewrite_question: str
answer: str
# 密集向量字段(embedding = 语义向量)
embedding: list[float] = Field(
json_schema_extra={'dim': 1024} # 每个向量 1024 个数字
)
# 稀疏向量字段(BM25 = 关键词向量)
content_sparse: list[float] = Field(
json_schema_extra={
'is_sparse_vector': True, # 这是稀疏向量
'bm25_source_field': 'question' # 从 question 字段生成
}
)
为什么有两种向量?
密集向量(embedding)= 语义理解
→ "特斯拉续航如何" ≈ "Model Y 能跑多远" ✅
→ 全靠 AI 模型的语义理解能力
稀疏向量(BM25)= 关键词匹配
→ "特斯拉" 命中 "特斯拉" ✅
→ "续航" 命中 "续航" ✅
→ 不管语义,只管关键词是否出现
两者混合搜索(RRF)= 语义 + 关键词全都要 ✅
混合搜索的代码逻辑:
python
results = VdbLLMConversation.get_n_high_similarity_item(
question="特斯拉电池续航如何", # 用户的问题
user_id="user_001", # 只在该用户的历史里找
n=5 # 返回最相似的 5 条
)
# 内部流程:
# 1. 把问题转成 1024 维向量(embedding)
# 2. 同时生成 BM25 稀疏向量(从关键词生成)
# 3. 密集向量搜索(权重 0.7)→ 得到一批结果
# 4. 稀疏向量搜索(权重 0.3)→ 得到一批结果
# 5. RRF 融合排名 → 综合得分最高的 5 条返回
3.4 VDBLLMKeyword ------ 关键词向量模型
作用:关键词的向量化存储,支持自然语言找相似关键词。
用户问:"什么是续航?"
1. 把问题转成向量
2. 找最接近的关键词向量
结果可能返回:
- "电池续航能力"(标准名称)
- "行驶里程"(同义词)
- "充电一次能跑多远"(语义相近)
→ AI 可以根据返回的关键词,更准确地理解用户想问什么
两种搜索方式的权重差异:
| 模型 | 密集权重 | 稀疏权重 | 为什么 |
|---|---|---|---|
| VdbLLMConversation | 0.7 | 0.3 | 对话更看重语义理解(说法多样) |
| VDBLLMKeyword | 0.3 | 0.7 | 关键词更看重精确匹配("续航"就是要找"续航") |
为什么关键词用更高的 BM25 权重?
关键词 = 专业术语 = 必须精确匹配
比如:监管文件里有"抵质押率"这个词
→ 用户问"抵押率"(口语化)
→ BM25 可能找到"抵质押率"(因为有重叠的字)
→ 但 embedding 可能认为"抵押率"和"抵质押率"差距较大
所以关键词表要用高 BM25 权重:确保专有名词能被发现
第4步:模型之间的关系------完整数据流
4.1 实体关系图
用户(user_001)
│
├──▶ BaseLLMSession(会话窗口)
│ │
│ ├── session_uuid = "abc123"
│ ├── title = "特斯拉问题讨论"
│ └── model_name = "qwen-plus"
│ │
│ └──▶ 多条 BaseLLMConversationModel(对话记录)
│ │
│ ├── question = "Model Y 续航多少?"
│ ├── answer = "约 550km"
│ └── status = "success"
│ │
│ └── 同时存入两个地方:
│ │
│ ├──▶ Milvus: VdbLLMConversation
│ │ embedding = [0.72, 0.15, ...]
│ │ 用于:下次语义相似度检索
│ │
│ └──▶ MySQL: BaseLLMConversationModel
│ 用于:分页查询、历史记录展示
│
└──▶ BaseKeywordModel(关键词)
│
├── keyword_name = "电池续航能力"
├── keyword_code = "BATTERY_RANGE"
└── keyword_synonyms = "续航,电池,电量"
│
└── 同时存入两个地方:
│
├──▶ Milvus: VDBLLMKeyword
│ embedding = [0.80, 0.12, ...]
│ 用于:自然语言找相关关键词
│
└──▶ MySQL: BaseKeywordModel
用于:关键词精确查找、管理页面
4.2 完整数据流转(从用户发问到得到答案)
Step 1: 用户发消息
"特斯拉电池续航怎么样?"
│
▼
Step 2: 找/建会话
BaseLLMSession.get_or_create_session(user_id="user_001")
│
▼
Step 3: 创建对话记录
BaseLLMConversationModel.create(
session_id="abc123",
question="特斯拉电池续航怎么样?"
)
│
▼
Step 4: 判断是否需要上下文(语义相似度搜索)
┌─────────────────────────────────┐
│ 如果这是第1次问(没有历史) │
│ → 跳过搜索,直接问 AI │
│ │
│ 如果不是第1次问 │
│ → VdbLLMConversation. │
│ get_n_high_similarity_item() │
│ → 找到最相似的3条历史对话 │
│ → 拼成上下文一起发给 AI │
└─────────────────────────────────┘
│
▼
Step 5: 调 AI
llm.invoke(context + "特斯拉电池续航怎么样?")
│
▼
Step 6: 保存回答
conversation.answer = AI 的回答
conversation.status = "success"
conversation.save() ← MySQL
│
▼
Step 7: 同步到向量库(异步)
VdbLLMConversation.embedding(
text=question + answer ← 生成向量
).batch_insert() ← 存入 Milvus
│
▼
Step 8: 返回回答给用户
第5步:Pydantic Field 详解
为什么要用 Pydantic
普通 Python 字典 vs Pydantic 模型:
python
# ❌ 普通字典------字段名可能写错、类型可能填错
user = {"name": "张三", "age": "25岁", "emial": "zhang@email.com"}
# emial 拼错了!age 是字符串不是整数!但 Python 不报错!
# ✅ Pydantic 模型------自动校验
class User(BaseModel):
name: str
age: int
email: str
user = User(name="张三", age="25岁", email="zhang@email.com")
# ❌ age="25岁" → 不是 int 类型 → Pydantic 自动报错!
# ❌ emial → 字段名不对 → Pydantic 自动报错!
Field 的三种写法
python
from pydantic import Field
class Student(BaseModel):
# 方式1: Field(None) = 可选字段,可以是 None
id: Optional[int] = Field(None, description="主键ID")
# 方式2: Field(...) = 必填字段,不能为空
name: str = Field(..., description="学生姓名")
# 方式3: Field(默认值) = 有默认值,不传就用默认值
age: int = Field(18, description="学生年龄")
gender: str = Field("未知", description="性别")
| 写法 | 代码示例 | 传参测试 |
|---|---|---|
| Field(None) | id: Optional[int] = Field(None) |
Student() ✅ Student(id=None) ✅ |
| Field(...) | name: str = Field(...) |
Student() ❌ 必须传 name |
| Field(默认值) | age: int = Field(18) |
Student(name="张三") ✅ age 自动=18 |
第6步:状态属性设计模式
问题:什么是"魔法数字"?
python
# ❌ 魔法数字------status == 1 是什么意思?
if session.status == 1:
print("会话可用")
# 1 = 什么? 2 = 什么? 时间久了谁记得住?
解决方案:用属性包装
python
class BaseKeywordModel:
status: int # 数据库里存的:0=禁用 1=可用 2=可用但不同步
# ✅ 用属性把数字变成语义
@property
def is_active(self) -> bool:
"""是否可用?True = status 为 1 或 2"""
return self.status in [1, 2]
@property
def can_sync_to_vdb(self) -> bool:
"""能否同步到向量库?True = 只有 status 为 1 时"""
return self.status == 1
使用对比:
python
# ❌ 魔法数字写法
if keyword.status == 1 or keyword.status == 2:
print("可用")
# ✅ 属性写法
if keyword.is_active:
print("可用")
# 更清晰,且如果业务规则变了(比如变成 status==1 才能用)
# 只需要改属性内部逻辑,不用改所有调用方
第7步:FAQ 深度解答
Q1:MySQL 和 Milvus 同时存数据,数据会不一致吗?
答:会有时间差,但不是"不一致"。
Save 操作时序:
MySQL.save() ──────▶ 立即返回(同步)
│
│ 同时
▼
Milvus.batch_insert() ──▶ 异步执行(可能有几毫秒延迟)
MySQL 是"正本",Milvus 是"索引":
- MySQL 存的是原始数据 → 必须准确
- Milvus 存的是向量 → 丢了可以从 MySQL 重建
如果 Milvus 插入失败了:
→ MySQL 已有数据
→ 可以手动触发一次"全量同步"从 MySQL 重建 Milvus 索引
Q2:向量维度(dim=1024)是怎么来的?
答:由 AI 模型决定。
不同的 embedding 模型输出不同维度的向量:
阿里云百炼(text-embedding-v3): 1024 维 ✅ 本项目用这个
OpenAI text-embedding-3-small: 1536 维
OpenAI text-embedding-3-large: 3072 维
MiniMax: 1536 维
Milvus 的向量字段必须声明维度:
embedding: list[float] = Field(json_schema_extra={'dim': 1024})
→ 如果存进去的向量不是 1024 维 → 报错
→ 搜索时传的向量不是 1024 维 → 报错
Q3:LRU 缓存在多进程环境下会怎样?
单进程(只有一个 Python 程序在跑):
Process A
└── _cached_get_param() 缓存存在 A 的内存里
✅ 正常
多进程(Nginx 起了 4 个 worker):
Process A ←── 有自己的缓存 ←── Process B ←── 有自己的缓存
└── _cached_get_param() └── _cached_get_param()
❌ 各自为政,缓存不共享!
场景:管理员改了参数
→ 管理员更新了数据库里的值
→ 清缓存只清了 Process A 的
→ Process B/C/D 还是旧的缓存 ❌
→ 不同用户访问,看到的值不一样!
解决方案:换 Redis 缓存(多进程共享)
┌─────────┐
│ Redis │ ← 所有进程共享同一个缓存
│ 缓存层 │
└────┬────┘
│
┌───┴───┬────┐
▼ ▼ ▼
ProcessA ProcessB ProcessC
Q4:向量搜索的权重(0.7/0.3)怎么调出来的?
答:经验 + 测试,没有标准答案。
调参过程:
Step 1: 定初始值
对话场景 → 参考论文和业界经验 → [0.7, 0.3]
Step 2: 建测试集
收集 200 条真实用户问题 + 人工标注的"正确答案"
Step 3: 批量测试
用不同权重组合搜索 → 看 Recall@K、MRR 等指标
Step 4: 选最优
[0.7, 0.3] → Recall@5 = 0.85 ← 当前选这个
[0.5, 0.5] → Recall@5 = 0.82
[0.3, 0.7] → Recall@5 = 0.78
Step 5: 上线观察
持续收集用户反馈,命中率低了就再调
Q5:为什么不把所有查询都换成向量搜索?
向量搜索很强,但不是万能的:
场景 A:我知道 session_id="abc123",想查这条记录
→ WHERE session_id = 'abc123' ✅ 精确查 → 快准狠
→ 向量搜索 ❌ "最像 abc123 的记录"------答非所问
场景 B:想查 2026年7月1日 创建的所有会话
→ WHERE created_at >= '2026-07-01' ✅ 范围查 → 数据库索引支持
→ 向量搜索 ❌ 按时间范围检索,数据库 B-Tree 最拿手
结论:
精确匹配/范围查询 → MySQL ✅
语义相似度搜索 → Milvus ✅
两者配合使用,各取所长
一句话总结
Models 层 = 系统的"数据模具"
┌─────────────────────────────────────────┐
│ │
│ 关系型模型(BaseModuleDBModel → MySQL) │
│ → 精确查询:WHERE / 分页 / 统计 │
│ → 类比:图书馆的索书号 │
│ │
│ 向量模型(BaseVDBModel → Milvus) │
│ → 语义搜索:找"最相似的" │
│ → 类比:图书馆的智能推荐 │
│ │
│ 两者配合 = 查得快 + 理解得准 │
│ │
└─────────────────────────────────────────┘