Initial commit
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
生成集成模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
|
||||
from langchain_community.chat_models.moonshot import MoonshotChat
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.runnables import RunnablePassthrough
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GenerationIntegrationModule:
|
||||
"""生成集成模块 - 负责LLM集成和回答生成"""
|
||||
|
||||
def __init__(self, model_name: str = "kimi-k2-0711-preview", temperature: float = 0.1, max_tokens: int = 2048):
|
||||
"""
|
||||
初始化生成集成模块
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
temperature: 生成温度
|
||||
max_tokens: 最大token数
|
||||
"""
|
||||
self.model_name = model_name
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.llm = None
|
||||
self.setup_llm()
|
||||
|
||||
def setup_llm(self):
|
||||
"""初始化大语言模型"""
|
||||
logger.info(f"正在初始化LLM: {self.model_name}")
|
||||
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("请设置 MOONSHOT_API_KEY 环境变量")
|
||||
|
||||
self.llm = MoonshotChat(
|
||||
model=self.model_name,
|
||||
temperature=self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
moonshot_api_key=api_key
|
||||
)
|
||||
|
||||
logger.info("LLM初始化完成")
|
||||
|
||||
def generate_basic_answer(self, query: str, context_docs: List[Document]) -> str:
|
||||
"""
|
||||
生成基础回答
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
context_docs: 上下文文档列表
|
||||
|
||||
Returns:
|
||||
生成的回答
|
||||
"""
|
||||
context = self._build_context(context_docs)
|
||||
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
你是一位专业的烹饪助手。请根据以下食谱信息回答用户的问题。
|
||||
|
||||
用户问题: {question}
|
||||
|
||||
相关食谱信息:
|
||||
{context}
|
||||
|
||||
请提供详细、实用的回答。如果信息不足,请诚实说明。
|
||||
|
||||
回答:""")
|
||||
|
||||
# 使用LCEL构建链
|
||||
chain = (
|
||||
{"question": RunnablePassthrough(), "context": lambda _: context}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
response = chain.invoke(query)
|
||||
return response
|
||||
|
||||
def generate_step_by_step_answer(self, query: str, context_docs: List[Document]) -> str:
|
||||
"""
|
||||
生成分步骤回答
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
context_docs: 上下文文档列表
|
||||
|
||||
Returns:
|
||||
分步骤的详细回答
|
||||
"""
|
||||
context = self._build_context(context_docs)
|
||||
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
你是一位专业的烹饪导师。请根据食谱信息,为用户提供详细的分步骤指导。
|
||||
|
||||
用户问题: {question}
|
||||
|
||||
相关食谱信息:
|
||||
{context}
|
||||
|
||||
请灵活组织回答,建议包含以下部分(可根据实际内容调整):
|
||||
|
||||
## 🥘 菜品介绍
|
||||
[简要介绍菜品特点和难度]
|
||||
|
||||
## 🛒 所需食材
|
||||
[列出主要食材和用量]
|
||||
|
||||
## 👨🍳 制作步骤
|
||||
[详细的分步骤说明,每步包含具体操作和大概所需时间]
|
||||
|
||||
## 💡 制作技巧
|
||||
[仅在有实用技巧时包含。优先使用原文中的实用技巧,如果原文的"附加内容"与烹饪无关或为空,可以基于制作步骤总结关键要点,或者完全省略此部分]
|
||||
|
||||
注意:
|
||||
- 根据实际内容灵活调整结构
|
||||
- 不要强行填充无关内容或重复制作步骤中的信息
|
||||
- 重点突出实用性和可操作性
|
||||
- 如果没有额外的技巧要分享,可以省略制作技巧部分
|
||||
|
||||
回答:""")
|
||||
|
||||
chain = (
|
||||
{"question": RunnablePassthrough(), "context": lambda _: context}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
response = chain.invoke(query)
|
||||
return response
|
||||
|
||||
def query_rewrite(self, query: str) -> str:
|
||||
"""
|
||||
智能查询重写 - 让大模型判断是否需要重写查询
|
||||
|
||||
Args:
|
||||
query: 原始查询
|
||||
|
||||
Returns:
|
||||
重写后的查询或原查询
|
||||
"""
|
||||
prompt = PromptTemplate(
|
||||
template="""
|
||||
你是一个智能查询分析助手。请分析用户的查询,判断是否需要重写以提高食谱搜索效果。
|
||||
|
||||
原始查询: {query}
|
||||
|
||||
分析规则:
|
||||
1. **具体明确的查询**(直接返回原查询):
|
||||
- 包含具体菜品名称:如"宫保鸡丁怎么做"、"红烧肉的制作方法"
|
||||
- 明确的制作询问:如"蛋炒饭需要什么食材"、"糖醋排骨的步骤"
|
||||
- 具体的烹饪技巧:如"如何炒菜不粘锅"、"怎样调制糖醋汁"
|
||||
|
||||
2. **模糊不清的查询**(需要重写):
|
||||
- 过于宽泛:如"做菜"、"有什么好吃的"、"推荐个菜"
|
||||
- 缺乏具体信息:如"川菜"、"素菜"、"简单的"
|
||||
- 口语化表达:如"想吃点什么"、"有饮品推荐吗"
|
||||
|
||||
重写原则:
|
||||
- 保持原意不变
|
||||
- 增加相关烹饪术语
|
||||
- 优先推荐简单易做的
|
||||
- 保持简洁性
|
||||
|
||||
示例:
|
||||
- "做菜" → "简单易做的家常菜谱"
|
||||
- "有饮品推荐吗" → "简单饮品制作方法"
|
||||
- "推荐个菜" → "简单家常菜推荐"
|
||||
- "川菜" → "经典川菜菜谱"
|
||||
- "宫保鸡丁怎么做" → "宫保鸡丁怎么做"(保持原查询)
|
||||
- "红烧肉需要什么食材" → "红烧肉需要什么食材"(保持原查询)
|
||||
|
||||
请输出最终查询(如果不需要重写就返回原查询):""",
|
||||
input_variables=["query"]
|
||||
)
|
||||
|
||||
chain = (
|
||||
{"query": RunnablePassthrough()}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
response = chain.invoke(query).strip()
|
||||
|
||||
# 记录重写结果
|
||||
if response != query:
|
||||
logger.info(f"查询已重写: '{query}' → '{response}'")
|
||||
else:
|
||||
logger.info(f"查询无需重写: '{query}'")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
def query_router(self, query: str) -> str:
|
||||
"""
|
||||
查询路由 - 根据查询类型选择不同的处理方式
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
|
||||
Returns:
|
||||
路由类型 ('list', 'detail', 'general')
|
||||
"""
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
根据用户的问题,将其分类为以下三种类型之一:
|
||||
|
||||
1. 'list' - 用户想要获取菜品列表或推荐,只需要菜名
|
||||
例如:推荐几个素菜、有什么川菜、给我3个简单的菜
|
||||
|
||||
2. 'detail' - 用户想要具体的制作方法或详细信息
|
||||
例如:宫保鸡丁怎么做、制作步骤、需要什么食材
|
||||
|
||||
3. 'general' - 其他一般性问题
|
||||
例如:什么是川菜、制作技巧、营养价值
|
||||
|
||||
请只返回分类结果:list、detail 或 general
|
||||
|
||||
用户问题: {query}
|
||||
|
||||
分类结果:""")
|
||||
|
||||
chain = (
|
||||
{"query": RunnablePassthrough()}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
result = chain.invoke(query).strip().lower()
|
||||
|
||||
# 确保返回有效的路由类型
|
||||
if result in ['list', 'detail', 'general']:
|
||||
return result
|
||||
else:
|
||||
return 'general' # 默认类型
|
||||
|
||||
def generate_list_answer(self, query: str, context_docs: List[Document]) -> str:
|
||||
"""
|
||||
生成列表式回答 - 适用于推荐类查询
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
context_docs: 上下文文档列表
|
||||
|
||||
Returns:
|
||||
列表式回答
|
||||
"""
|
||||
if not context_docs:
|
||||
return "抱歉,没有找到相关的菜品信息。"
|
||||
|
||||
# 提取菜品名称
|
||||
dish_names = []
|
||||
for doc in context_docs:
|
||||
dish_name = doc.metadata.get('dish_name', '未知菜品')
|
||||
if dish_name not in dish_names:
|
||||
dish_names.append(dish_name)
|
||||
|
||||
# 构建简洁的列表回答
|
||||
if len(dish_names) == 1:
|
||||
return f"为您推荐:{dish_names[0]}"
|
||||
elif len(dish_names) <= 3:
|
||||
return f"为您推荐以下菜品:\n" + "\n".join([f"{i+1}. {name}" for i, name in enumerate(dish_names)])
|
||||
else:
|
||||
return f"为您推荐以下菜品:\n" + "\n".join([f"{i+1}. {name}" for i, name in enumerate(dish_names[:3])]) + f"\n\n还有其他 {len(dish_names)-3} 道菜品可供选择。"
|
||||
|
||||
def generate_basic_answer_stream(self, query: str, context_docs: List[Document]):
|
||||
"""
|
||||
生成基础回答 - 流式输出
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
context_docs: 上下文文档列表
|
||||
|
||||
Yields:
|
||||
生成的回答片段
|
||||
"""
|
||||
context = self._build_context(context_docs)
|
||||
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
你是一位专业的烹饪助手。请根据以下食谱信息回答用户的问题。
|
||||
|
||||
用户问题: {question}
|
||||
|
||||
相关食谱信息:
|
||||
{context}
|
||||
|
||||
请提供详细、实用的回答。如果信息不足,请诚实说明。
|
||||
|
||||
回答:""")
|
||||
|
||||
chain = (
|
||||
{"question": RunnablePassthrough(), "context": lambda _: context}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
for chunk in chain.stream(query):
|
||||
yield chunk
|
||||
|
||||
def generate_step_by_step_answer_stream(self, query: str, context_docs: List[Document]):
|
||||
"""
|
||||
生成详细步骤回答 - 流式输出
|
||||
|
||||
Args:
|
||||
query: 用户查询
|
||||
context_docs: 上下文文档列表
|
||||
|
||||
Yields:
|
||||
详细步骤回答片段
|
||||
"""
|
||||
context = self._build_context(context_docs)
|
||||
|
||||
prompt = ChatPromptTemplate.from_template("""
|
||||
你是一位专业的烹饪导师。请根据食谱信息,为用户提供详细的分步骤指导。
|
||||
|
||||
用户问题: {question}
|
||||
|
||||
相关食谱信息:
|
||||
{context}
|
||||
|
||||
请灵活组织回答,建议包含以下部分(可根据实际内容调整):
|
||||
|
||||
## 🥘 菜品介绍
|
||||
[简要介绍菜品特点和难度]
|
||||
|
||||
## 🛒 所需食材
|
||||
[列出主要食材和用量]
|
||||
|
||||
## 👨🍳 制作步骤
|
||||
[详细的分步骤说明,每步包含具体操作和大概所需时间]
|
||||
|
||||
## 💡 制作技巧
|
||||
[仅在有实用技巧时包含。如果原文的"附加内容"与烹饪无关或为空,可以基于制作步骤总结关键要点,或者完全省略此部分]
|
||||
|
||||
注意:
|
||||
- 根据实际内容灵活调整结构
|
||||
- 不要强行填充无关内容
|
||||
- 重点突出实用性和可操作性
|
||||
|
||||
回答:""")
|
||||
|
||||
chain = (
|
||||
{"question": RunnablePassthrough(), "context": lambda _: context}
|
||||
| prompt
|
||||
| self.llm
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
for chunk in chain.stream(query):
|
||||
yield chunk
|
||||
|
||||
def _build_context(self, docs: List[Document], max_length: int = 2000) -> str:
|
||||
"""
|
||||
构建上下文字符串
|
||||
|
||||
Args:
|
||||
docs: 文档列表
|
||||
max_length: 最大长度
|
||||
|
||||
Returns:
|
||||
格式化的上下文字符串
|
||||
"""
|
||||
if not docs:
|
||||
return "暂无相关食谱信息。"
|
||||
|
||||
context_parts = []
|
||||
current_length = 0
|
||||
|
||||
for i, doc in enumerate(docs, 1):
|
||||
# 添加元数据信息
|
||||
metadata_info = f"【食谱 {i}】"
|
||||
if 'dish_name' in doc.metadata:
|
||||
metadata_info += f" {doc.metadata['dish_name']}"
|
||||
if 'category' in doc.metadata:
|
||||
metadata_info += f" | 分类: {doc.metadata['category']}"
|
||||
if 'difficulty' in doc.metadata:
|
||||
metadata_info += f" | 难度: {doc.metadata['difficulty']}"
|
||||
|
||||
# 构建文档文本
|
||||
doc_text = f"{metadata_info}\n{doc.page_content}\n"
|
||||
|
||||
# 检查长度限制
|
||||
if current_length + len(doc_text) > max_length:
|
||||
break
|
||||
|
||||
context_parts.append(doc_text)
|
||||
current_length += len(doc_text)
|
||||
|
||||
return "\n" + "="*50 + "\n".join(context_parts)
|
||||
Reference in New Issue
Block a user