答疑机器人
大模型如何工作
工作流程
- 输入文本分词化
Token:分词,具有独立语义的词语,每个Token分配一个ID。 - Token向量化。
- 推理
- 循环输出Token==》输出文本
影响大模型内容生成的随机性参数
- temperature:温度从低到高(0.1 -> 0.7 -> 1.2),概率分布从陡峭趋于平滑,输出也会从相对固定 到逐渐多样化。
- top_p:范围在0~1。从候选 Token 集合中选出符合条件的"小集合"。具体方法是:按概率从高到低排序,选取累计概率达到设定阈值的 Token 组成新的候选集合,从而缩小选择范围。
- 值越大 :候选范围越广,内容更多样化,适合创意写作、诗歌生成等场景。
- 值越小 :候选范围越窄,输出更稳定,适合新闻初稿、代码生成等需要明确答案的场景。
- top_k:范围在>=1。从概率排名前k的Token中随机选择一个进行输出。一般来说,top_k越大,生成内容越多样化;top_k越小,内容则更固定。
- seed:每次模型调用时传入相同的seed值,并保持其他参数不变,模型会尽最大可能返回相同结果。
RAG应用-扩展答疑机器人的知识范围
RAG的工作原理
RAG:Retrieval Argumented Generation
建立索引
文档解析==》分段(切片)==》向量化==》存储索引(向量数据库)
python
# 加载文档并解析
documents = SimpleDirectoryReader('./docs').load_data()
# 建立索引(包含切片)
index = VectorStoreIndex.from_documents(
documents,
# 指定embedding 模型
embed_model=DashScopeEmbedding(
model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2
))
检索+生成
- 检索:通过Embedding模型比对文本,也可以做rerank、句子窗口检索方法。
- 生成:利用大模型的总结能力,根据问题和文本段生成回答。
多轮对话
如果将完整历史对话与问题都输入到检索系统,由于字数较多,检索系统可能无法处理(embedding模型在长文本上效果差于短文本)。业界常用的解决方法是:
- 通过大模型,基于历史对话信息,将用户的问题改写为一个新的query,新的query将包含历史对话的关键信息。
- 使用新的query,按照原先流程进行检索与生成的过程。
python
chat_engine = CondenseQuestionChatEngine.from_defaults(
# 查询引擎
query_engine=query_engine,
# 提示词模板
condense_question_prompt=custom_prompt,
# 历史对话记录
chat_history=custom_chat_history,
llm=OpenAILike(
model="qwen-plus-0919",
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key=os.getenv("DASHSCOPE_API_KEY"),
is_chat_model=True
),
verbose=True
streaming_response = chat_engine.stream_chat("核心职责是什么")
)
提示词工程-优化提示词改善答疑机器人回答质量
提示词在很大程度上决定了大模型的回答质量,接下来你可以参考一些提示词框架构建提示词。
提示词框架
python
from chatbot import rag
# 加载索引
index = rag.load_index()
query_engine = rag.create_query_engine(index=index)
# 问答
streaming_response = query_engine.query(question)
streaming_response.print_response_stream()
基本要素
提示词中需要明确以下几个要素:任务目标、上下文、角色、受众、样例、输出格式。这些要素构成了一个提示词框架,能帮助你构建一个完整、有效的提示词。
要素 | 含义 |
---|---|
任务目标(Object) | 明确要求大模型完成什么任务,让大模型专注具体目标 |
上下文(Context) | 任务的背景信息,比如操作流程、任务场景等,明确大模型理解讨论的范围 |
角色(Role) | 大模型扮演的角色,或者强调大模型应该使用的语气、写作风格等,明确大模型回应的预期情感 |
受众(Audience) | 明确大模型针对的特定受众,约束大模型的应答风格 |
样例(Sample) | 让大模型参考的具体案例,大模型会从中抽象出实现方案、需要注意的具体格式等信息 |
输出格式(Output Format) | 明确指定输出的格式、输出类型、枚举值的范围。通常也会明确指出不需要输出的内容和不期望的信息,可以结合样例来进一步明确输出的格式和输出方法 |
提示词自动优化工具:bailian.console.aliyun.com/?tab=app#/c...
提示词模板
直接让用户根据框架书写提示词并非最佳选择。
python
# 构建提示词模板
prompt_template_string = (
"你是公司的客服小蜜,你需要简明扼要的回答用户的问题"
"【注意事项】:\n"
"1. 依据上下文信息来回答用户问题。\n"
"2. 你只需要回答用户的问题,不要输出其他信息\n"
"以下是参考信息。"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"问题:{query_str}\n。"
"回答:"
)
# 更新提示词模板
rag.update_prompt_template(query_engine,prompt_template_string)
构建有效提示词的技巧
- 清晰表达需求,并使用分隔符。
- 限定角色和受众。
角色:大模型扮演什么角色。
受众:用户扮演什么角色。 - 提供少量样本示例。
- 给模型"思考"的时间。 思维连(COT)是让大模型进行思考的一种方式。但是紧靠"思考"无法完成更复杂的工作,大模型也从思维连到多智能体发展。
使用大模型做意图识别
让大模型进行意图识别也有以下两种方法:
- 使用提示词:设计特定的提示词,引导大模型生成符合预期的回答。无需要修改模型参数,而是依靠构造的输入来激发模型内部已有的知识。
- 微调模型:使用特定的标注数据进一步训练模型,使其更好的对意图进行分类。
推理大模型
python
from openai import OpenAI
# 初始化客户端
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
自动化评测
RAG自动化评测体系
从以下几个维度评估:
- 召回质量:【检索】正确且相关
- 答案忠实度:【生成】基于检索的上下文
- 答案相关性:【生成】
- 上下文利用率/效率:是否有效利用上下文。
评测框架:Ragas、Trulens、DeepEval
使用Ragas评估
需要准备question、ground_truth。
- Answer Correctness的计算过程。
- 语义相似度:计算answer和ground_truth的文本向量相似度。
- 事实相似度:Ragas衡量事实准确度的方法,维护TP、FP、FN列表。
- 召回效果:参考信息的准确度。
- 需要准备:question、context、ground_truth
- Context precision:召回的context中,与ground_truth相关的context是否排名靠前。
- context中,有多少context与ground_truth相关,以及context的排名情况。
- Context recall:context与ground_truth的一致性,侧重事实准确度。
- ground_truth 中有多少比例的观点可以得到 contexts 的支持。
如何根据Ragas指标进行优化
- 上下文是RAG的生命线。
- Lost in the Middle:在海量的无关信息里,大模型对关键信息"视而不见"。
- 知识浓度:相关信息密度高、噪音少、与问题直接关联。
- Context recall。 如果Context recall较低,可以考虑:
- 检查知识库;
- 更换Embedding模型
- query改写(提示词工程)
- Context precision。 如果Context precision较低,可以考虑:
- 检索阶段增加rerank,提升相关文本段的排名。
- Answer Correctness。 如果Answer Correctness较低,而前两个指标较高,则考虑:
- 优化提示词;
- 调整temperature等超参;
- 更换性能强大的大模型;
- 微调
打造卓越的评测体系
- 业务专家参与
- 从用户视角出发
- 持续运营
- 维护评测集。
建议在实际应用时,邀请 RAG 应用对应的领域专家(人工参与)一起构建能反映真实场景问题分布的测试集,并且持续更新测试集。
优化RAG提升准确度
初步优化检索结果
让大模型获得更多的参考信息
调整代码,检索引擎召回的切片数增加。
==》召回的文档切片存在无关信息,且有效信息未被完全召回。
给大模型结构更清晰的参考信息
Markdown格式是一个很好的选择。(需要重建索引) ==》回答准确度能够提高。
RAG的工作流程
文档解析与切片、向量存储、检索与召回参考信息、生成答案。
RAG应用各个环节与改进策略
文档准备阶段
- 识别意图空间和知识空间。
- 构建一套可以持续收集用户意图的机制,从而完善知识库,邀请专家参与评测,形成"数据采集-知识更新-专家验证"的闭环流程。
文档解析与切片阶段
- 解析:百炼DashScopeParse解析PDF、Word文档。
- 切片:切片方式影响召回质量。
- 文档切片缺少关键信息:回答不准确。
- 文档切片非关联信息过多(噪声):影响回答质量。
Token切片、句子切片、句子窗口切片、语义切片、Markdown切片
类别 | 细分类型 | 改进策略 | 场景化示例 |
---|---|---|---|
文档解析 | 文档类型不统一,部分格式的文档不支持解析 比如前面用到的 SimpleDirectoryLoader 并不支持 Keynote 格式的文件 | 开发对应格式的解析器,或转换文档格式 | 例如,某公司使用了大量的 Keynote 文件存储员工信息,但现有的解析器不支持 Keynote 格式。可以开发 Keynote 解析器或将文件转换为支持的格式(如 PDF)。 |
文档解析 | 已支持解析的文档格式里,存在一些特殊内容 比如文档里嵌入了表格、图片、视频等 | 改进文档解析器 | 例如,某文档中包含了大量的表格和图片,现有解析器无法正确提取表格中的信息。可以改进解析器,使其能够处理表格和图片。 |
文档解析 | ... | ... | ... |
文档切片 | 文档中有很多主题接近的内容 比如工作手册文档中,需求分析、开发、发布等每个阶段都有注意事项、操作指导 | 扩写文档标题及子标题 「注意事项」=>「需求分析>注意事项」 建立文档元数据(打标) | 例如,某文档中包含多个阶段的注意事项,用户提问"需求分析的注意事项是什么?"时,系统返回了所有阶段的注意事项。可以通过扩展标题和打标来区分不同阶段的内容。 |
文档切片 | 文档切片长度过大,引入过多干扰项 | 减少切片长度,或结合具体业务开发为更合适的切片策略 | 例如,某文档的切片长度过大,包含了多个不相关的主题,导致检索时返回了无关信息。可以减少切片长度,确保每个切片只包含一个主题。 |
文档切片 | 文档切片长度过短,有效信息被截断 | 扩大切片长度,或结合具体业务开发为更合适的切片策略 | 例如,某文档中每个切片只有一句话,导致检索时无法获取完整的上下文信息。可以增加切片长度,确保每个切片包含完整的上下文。 |
文档切片 | ... | ... | ... |
切片向量化与存储阶段
文档切片后,需要建立索引,以便后续检索。一种常见方法是使用Embedding模型将切片向量化。
- 选择合适Embedding模型。
百炼的text-embedding-v2、text-embedding-v3。 - 选择合适的向量数据库:内存向量数据库、本地向量数据库、云服务向量数据库(Milvus)
检索召回阶段
主要问题:找出与问题最相关、包含正确答案的切片。
- 用户问题不完整、有歧义。==》想办法还原用户意图。
- 检索存在无关信息。==》减少无关信息,避免影响答案生成。
解决:
- 检索前:问题改写、问题扩充、提取标签、反问用户、思考并规划多次检索。
- 检索后:重排序+过滤、句子窗口检索。
重点:
- 问题改写:
1)使用大模型扩充用户问题;
2)将单一查询改为多步骤查询。
StepDecomposeQueryTransform:复杂问题分解为多步骤; MultiStepQueryEngine:多步骤查询引擎。
3)假想文档(Hypothetical Document Embeddings) - 重排序:从向量数据库检索召回3条相关文档片段,但是这3条不一定是事实相关的。百炼提供文本排序模型,对文档做重排序,筛选最相关的3条。
- 提取标签:建立索引时,可以将标签与文档切片一起存储,这种"标签过滤+向量检索"的组合方式,能大幅提升检索准确性。
时机 | 改进策略 | 示例 |
---|---|---|
检索前 | 问题改写 | 「附近有好吃的餐厅吗?」=> 「请推荐我附近的几家评价较高的餐厅」 |
检索前 | 问题扩写 通过增加更多信息,让检索结果更全面 | 「张伟是哪个部门的?」=> 「张伟是哪个部门的?他的联系方式、职责范围、工作目标是什么?」 |
检索前 | 基于用户画像扩展上下文 结合用户信息、行为等数据扩写问题 | 内容工程师提问「工作注意事项」=> 「内容工程师有哪些工作注意事项」 项目经理提问「工作注意事项」=> 「项目经理有哪些工作注意事项」 |
检索前 | 提取标签 提取标签,用于后续标签过滤+向量相似度检索 | 「内容工程师有哪些工作注意事项」=> * 标签过滤:{"岗位": "内容工程师"} * 向量检索:「内容工程师有哪些工作注意事项」 |
检索前 | 反问用户 | 「工作职责是什么」=> 大模型反问:「请问你想了解哪个岗位的工作职责」 实现反问的提示词可以参考: 10分钟构建能主动提问的智能导购 |
检索前 | 思考并规划多次检索 | 「张伟不在,可以找谁」 => 大模型思考规划: => task_1:张伟的职责是什么, task_2:${task_1_result}职责的人有谁 => 按顺序执行多次检索 |
检索前 | ... | ... |
检索后 | 重排序 ReRank + 过滤 多数向量数据库会考虑效率,牺牲一定精确度,召回的切片中可能有一些实际相关性不够高 | chunk1、chunk2...、chunk10 => chunk 2、chunk4、chunk5 |
检索后 | 滑动窗口检索 在检索到一个切片后,补充前后相邻的若干个切片。这样做的原因是:相邻切片之间往往存在语义联系,仅看单个切片可能会丢失重要信息。 滑动窗口检索确保了不会因为过度切分而丢失文本间的语义连接。 | 常见的实现是句子滑动窗口,你可以用下方的简化形式来理解: 假设原始文本为:ABCDEFG(每个字母代表一个句子) 当检索到切片:D 补充相邻切片后:BCDEF(前后各取2个切片) 这里的BC和EF是D的上下文。比如: * BC可能包含解释D的背景信息 * EF可能包含D的后续发展或结果 * 这些上下文信息能帮助你更准确地理解D的完整含义 通过召回这些相关的上下文切片,你可以提高检索结果的准确性和完整性。 |
检索后 | ... | ... |
生成答案阶段
- 选择合适的大模型。
- 优化提示词模板:
- 要求不编造答案。
- 添加分隔标记。
- 先识别问题类型,后映射到不同的提示词模板,即根据问题类型调整模板。
- 调整大模型超参:seed、presense_penalty(重复惩罚)、temperature、top_p、max_tokens
插件-Agent
智能体不仅能够与外界交互,还能处理复杂任务,几个核心模块:
- 工具模块:定义和管理工具
- 记忆模块:
- 长期记忆:帮助智能体学习。
- 短期记忆:临时存储当前任务信息。
- 计划能力
- 决策、规划
- 行动能力
- 与工具模块紧密结合,执行任务
python
from dashscope import Assistants, Messages, Runs, Threads
ChatAssistant = Assistants.create(
# 在此指定模型名称
model="qwen-plus",
# 在此指定Agent名称
name='公司小蜜',
# 在此指定Agent的描述信息
description='一个智能助手,能够查询员工信息,帮助员工发送请假申请,或者查询公司规章制度。',
# 用于提示大模型所具有的工具函数能力,也可以规范输出格式
instructions='''你是公司小蜜,你的功能有以下三个:
1. 查询员工信息。例如:查询员工张三的HR是谁;
2. 发送请假申请。例如:当员工提出要请假时,你可以在系统里帮他完成请假申请的发送;
3. 查询公司规章制度。例如:我们公司项目管理的工具是什么?
请准确判断需要调用哪个工具,并礼貌回答用户的提问。
''',
# 将工具函数传入
tools=[
{
# 定义工具函数类型,一般设置为function即可
'type': 'function',
'function': {
# 定义工具函数名称,通过map方法映射到query_employee_info函数
'name': '查询员工信息',
# 定义工具函数的描述信息,Agent主要根据description来判断是否需要调用该工具函数
'description': '当需要查询员工信息时非常有用,比如查询员工张三的HR是谁,查询教育部门总人数等。',
# 定义工具函数的参数
'parameters': {
'type': 'object',
'properties': {
# 将用户的提问作为输入参数
'query': {
'type': 'str',
# 对输入参数的描述
'description': '用户的提问。'
},
},
# 在此声明该工具函数需要哪些必填参数
'required': ['query']},
}
}
]
)
# 提前定义好工具函数query_employee_info
function_mapper = {
"查询员工信息": query_employee_info
}
new_tool = {'type': 'function',
'function': {
'name': '发送请假申请',
'description': '当需要帮助员工发送请假申请时非常有用。',
'parameters': {
'type': 'object',
'properties': {
# 需要请假的时间
'date': {
'type': 'str',
'description': '员工想要请假的时间。'
},
},
'required': ['date']},
}
}
ChatAssistant.tools.append(new_tool)
# 提前定义好一个工具函数send_leave_application
function_mapper["发送请假申请"] = send_leave_application
多智能体
当机器人需要在一个请求中执行多个操作时,单个智能体可能无法有效完成所有子任务。
- planner agent
python
planner_agent = Assistants.create(
model="qwen-plus",
name='流程编排机器人',
description='你是团队的leader,你的手下有很多agent,你需要根据用户的输入,决定要以怎样的顺序去使用这些agent'
)
# 改进
planner_agent=Assistants.update(planner_agent.id,instructions="""你的团队中有以下agent。
employee_info_agent:可以查询公司的员工信息,如果提问中关于部门、HR等信息,则调用该agent;
leave_agent:可以帮助员工发送请假申请,如果用户提出请假,则调用该agent;
chat_agent:如果用户的问题无需以上agent,则调用该agent。
你需要根据用户的问题,判断要以什么顺序使用这些agent,一个agent可以被多次调用。你的返回形式是一个列表,不能返回其它信息。比如:["employee_info_agent", "leave_agent"]或者["chat_agent"],列表中的元素只能为上述的agent。""")
将任务规划结果转化为列表对象,逐步解析每个步骤。
python
import ast
# 使用Planner Agent获取任务规划
planner_response = get_agent_response(planner_agent, "王五在哪个部门?帮我提交下周三请假的申请")
# Planner Agent返回的是一个描述调用顺序的列表形式字符串
order_stk = ast.literal_eval(planner_response)
- 工具函数
- summary agent
python
summary_agent = Assistants.create(
model="qwen-plus",
name='总结机器人',
description='一个智能助手,根据用户的问题与参考信息,全面、完整地回答用户问题',
instructions='你是一个智能助手,根据用户的问题与参考信息,全面、完整地回答用户问题'
)
完整流程
python
# 获取Agent的运行顺序
agent_order = get_agent_response(planner_agent,query)
order_stk = ast.literal_eval(agent_order)
cur_query = query
# 依次运行Agent
for i in range(len(order_stk)):
cur_agent = agent_mapper[order_stk[i]]
response = get_agent_response(cur_agent,cur_query)
Agent_Message += f"*{order_stk[i]}*的回复为:{response}\n\n"
...
# 如果当前Agent为最后一个Agent,则将其输出作为Multi Agent的输出
# 如果当前Agent不是最后一个Agent,则将上一个Agent的输出response添加到下一轮的query中,作为参考信息
多智能体编排功能
智能体流程画布的开创者:Dify.ai
- 用户直观看到各个智能体的执行规则和链路。
- 编排多个智能体的协作
- 快速验证效果