【灶台导航】ai回答全流程架构
全流程架构图
逐环节详解 + 对应代码
① 前端:用户输入 → 云函数调用
文件: miniprogram/pages/index/index.js:377-407
// 用户点击发送
onSend() {
const userInput = this.data.inputText.trim();
this.callAI(userInput);
}
// 调用云函数
async callAI(userInput) {
const userProfile = this.data.currentMember ? {
roleName: this.data.currentMember.name,
preferences: this.data.currentMember.preferences || [],
allergies: this.data.currentMember.allergies || []
} : {};
const res = await callFunction('chat', {
sessionId: this.data.sessionId,
message: userInput,
userProfile: userProfile
});
}
要点: 前端只负责采集输入和上下文,不参与任何AI逻辑。
② 云函数入口:路由 + 会话管理 + 超时控制
文件: cloudfunctions/chat/index.js:205-248
exports.main = async (event, context) => {
const openid = context.OPENID || 'anonymous'
return await withTimeout(processChat(event, openid), 34000, '云函数执行超时')
}
文件: cloudfunctions/chat/index.js:251-357
async function processChat(event, openid) {
// 1. 加载/创建会话(多轮对话历史)
// 2. 截取最近20条历史,防止token溢出
// 3. 构建用户上下文字符串
let userContext = ''
if (userProfile) {
if (userProfile.roleName) parts.push(`当前用餐人:${userProfile.roleName}`)
if (userProfile.preferences.length) parts.push(`口味偏好:${userProfile.preferences.join('、')}`)
if (userProfile.allergies.length) parts.push(`忌口:${userProfile.allergies.join('、')}`)
}
// 4. 【核心】RAG检索 + LLM调用
const { contextText, ragRecipes } = await retrieveContext(message, openid)
const aiResult = await callDeepSeekAPI(..., contextText, ragRecipes, ...)
// 5. 保存会话历史到数据库
// 6. 返回结果给前端
}
③ RAG三级检索:retrieveContext()
文件: cloudfunctions/chat/index.js:116-201
async function retrieveContext(message, openid) {
let systemRagRecipes = []
let contextText = ''
// ═══ 第1路:Qdrant 向量语义搜索(主链路) ═══
try {
const vectorResults = await withTimeout(
qdrant.vectorSearch(message, 3),
TIMEOUT_CONFIG.embedding + TIMEOUT_CONFIG.qdrant,
'向量检索超时'
)
if (vectorResults && vectorResults.length > 0) {
const relevant = vectorResults.filter(r => r.similarity > 0.3)
if (relevant.length > 0) {
systemRagRecipes = relevant.map(r => r.recipe)
contextText = formatContext(relevant)
}
}
} catch (e) {
console.warn('[RAG-Vector] 向量检索失败,降级为 TF-IDF:', e.message)
}
// ═══ 第2路:TF-IDF 降级(仅在系统菜谱无结果时) ═══
if (systemRagRecipes.length === 0) {
const { data: recipes } = await db.collection('recipes')
.where({ isPrivate: _.neq(true) })
.limit(50).get()
const results = search(message, recipes, 3)
const relevant = results.filter(r => r.similarity > 0.05)
// ... 赋值 systemRagRecipes + contextText
}
// ═══ 第3路:私有菜谱语义检索(仅当前用户) ═══
let privateRagRecipes = []
if (openid && openid !== 'anonymous') {
const privateResults = await withTimeout(
qdrant.searchPrivateRecipes(message, openid, 2),
...
)
const relevant = privateResults.filter(r => r.similarity > 0.3)
if (relevant.length > 0) {
privateRagRecipes = relevant.map(r => r.recipe)
contextText += '\n\n以下是用户的私人菜谱库中的相关菜谱:\n'
contextText += formatContext(relevant)
}
}
return { contextText, ragRecipes: [...systemRagRecipes, ...privateRagRecipes] }
}
第1路细节:Qdrant向量搜索
文件: cloudfunctions/chat/qdrant.js:89-117
async function vectorSearch(message, topK = 3) {
const queryVector = await getEmbedding(message) // 调用SiliconFlow API
const hits = await searchQdrant(queryVector, topK)
return hits.map(hit => ({
recipe: { _id: hit.payload.recipeId, name: hit.payload.name, ... },
similarity: hit.score
}))
}
Embedding调用 (qdrant.js:27-48):
async function getEmbedding(text) {
const response = await got.post('https://api.siliconflow.cn/v1/embeddings', {
json: {
model: 'BAAI/bge-m3', // 开源Embedding模型
input: text,
encoding_format: 'float'
}
})
return result.data[0].embedding // 返回1024维float数组
}
Qdrant搜索 (qdrant.js:58-79):
async function searchQdrant(queryVector, topK = 3) {
const response = await got.post(
'http://47.122.88.208:6333/collections/recipes/points/query',
{
json: {
query: queryVector,
limit: topK,
with_payload: true
}
}
)
return hits
}
第3路细节:私有菜谱带过滤的向量搜索
文件: cloudfunctions/chat/qdrant.js:167-207
async function searchPrivateRecipes(message, openid, topK = 2) {
const queryVector = await getEmbedding(message)
const response = await got.post(
'.../collections/private_recipes/points/query',
{
json: {
query: queryVector,
limit: topK,
with_payload: true,
filter: {
must: [{ key: 'openid', match: { value: openid } }]
}
}
}
)
return hits.map(hit => ({ recipe: {..., isPrivate: true}, similarity: hit.score }))
}
RAG上下文格式化
文件: cloudfunctions/chat/tfidf.js:224-239
function formatContext(searchResults) {
const lines = ['以下是系统菜谱库中的相关菜谱,请优先推荐...']
searchResults.forEach((item, i) => {
const r = item.recipe
const idTag = r._id ? `[ID:${r._id}] ` : '' // ID标签,让LLM引用
const privateTag = r.isPrivate ? '[私人菜谱] ' : ''
lines.push(`${i + 1}. ${privateTag}${idTag}${r.name}(${r.difficulty},${r.cookTime}分钟):${r.description}`)
lines.push(` 食材:${ingredients}`)
})
return lines.join('\n')
}
④ 上下文注入:构建LLM的Prompt
文件: cloudfunctions/chat/index.js:371-392
async function callDeepSeekAPI(history, userMessage, userContext, ragContext, ragRecipes, generateMode) {
let systemContent = SYSTEM_PROMPT // 第57-106行,定义角色+输出格式
if (ragContext) {
systemContent += '\n\n' + ragContext
}
const messages = [{ role: 'system', content: systemContent }]
for (const item of history) {
messages.push({ role: item.role, content: item.content })
}
const fullMessage = userMessage + userContext
messages.push({ role: 'user', content: fullMessage })
// → 发送给 DeepSeek
}
System Prompt (chat/index.js:57-106) 定义了:
- 角色定位(烹饪助手)
- 强制JSON输出格式:
response_format: { type: 'json_object' } - 4种动作类型:
recommend|ask|generateRecipe|cook - 菜谱推荐规则(系统菜谱 vs AI生成 vs 未收录)
fullRecipe的数据结构定义
⑤ LLM调用:DeepSeek API
文件: cloudfunctions/chat/index.js:395-410
const response = await got.post('https://api.deepseek.com/chat/completions', {
headers: { 'Authorization': `Bearer ${DEEPSEEK_CONFIG.apiKey}` },
json: {
model: 'deepseek-chat',
messages: messages,
temperature: 0.7,
max_tokens: generateMode ? 2048 : 512,
response_format: { type: 'json_object' }
},
timeout: { request: 30000 }
})
⑥ 响应解析:LLM输出 → 结构化数据
文件: cloudfunctions/chat/index.js:412-443
const aiContent = responseBody.choices[0].message.content
const parsed = parseAIReply(aiContent)
// 返回: { reply, action, recommendations, cookData }
parsed.recommendations = await resolveRecipeIds(parsed.recommendations, ragRecipes)
resolveRecipeIds (chat/index.js:493-520):
| 情况 | 来源 | 处理 |
|---|---|---|
AI返回了[ID:xxx] |
database |
直接映射 |
| AI生成了完整菜谱 | ai-generated |
分配临时ID |
| 菜名能匹配RAG结果 | database |
映射到真实ID |
| 完全无匹配 | not-found |
前端显示"未收录" |
⑦ 前端展示
文件: miniprogram/pages/index/index.js:442-467
const aiReply = res.data;
const aiMessage = {
role: 'ai',
content: aiReply.reply,
recommendations: aiReply.recommendations || []
};
this.setData({ chatMessages: [...this.data.chatMessages, aiMessage] });
WXML (index.wxml:14-34): 根据每个推荐菜谱的 source 字段显示不同标签(AI生成 / 未收录),以及"保存菜谱"按钮。
⑧ 保存到私人菜谱(反向写入向量库)
文件: miniprogram/pages/index/index.js:536-610
async saveToMyRecipes(e) {
// 查找匹配的推荐菜谱
const recipeData = { name, description, prepTime, cookTime, ingredients, steps, ... }
const res = await callFunction('userProfile', {
action: 'addPrivateRecipe',
data: recipeData
});
}
userProfile云函数 (cloudfunctions/userProfile/index.js:426-461):
async function addPrivateRecipe(openid, recipeData) {
// 1. 写入云数据库
const recipe = { ...recipeData, openid, isPrivate: true, ... }
const { id: recipeId } = await db.collection('recipes').add({ data: recipe })
// 2. 【异步】生成Embedding并写入Qdrant
qdrantPrivate.upsertPrivateRecipe(recipe, openid).catch(err => { ... })
}
向量入库 (cloudfunctions/userProfile/qdrant-private.js:74-118):
async function upsertPrivateRecipe(recipe, openid) {
const text = parts.join(' ') // 名称+描述+标签+食材
const vector = await getEmbedding(text)
await got.put('.../collections/private_recipes/points', {
json: {
points: [{
id: pointId,
vector: vector,
payload: { recipeId: recipe._id, openid, name, ... }
}]
}
})
}
这样下次用户聊天时,③中的第3路检索就能从
private_recipes集合中语义召回这道私人菜谱。
降级容错链路总结
每一层都有独立的超时控制(
withTimeout),保证云函数35秒内必须返回结果。
更多推荐
所有评论(0)