答疑机器人
大模型如何工作
工作流程
- 输入文本分词化
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
建立索引
文档解析==》分段(切片)==》向量化==》存储索引(向量数据库)
# 加载文档并解析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,按照原先流程进行检索与生成的过程。
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=Truestreaming_response = chat_engine.stream_chat("核心职责是什么"))
提示词工程-优化提示词改善答疑机器人回答质量
提示词在很大程度上决定了大模型的回答质量,接下来你可以参考一些提示词框架构建提示词。
提示词框架
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…
提示词模板
直接让用户根据框架书写提示词并非最佳选择。
# 构建提示词模板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)是让大模型进行思考的一种方式。但是紧靠“思考”无法完成更复杂的工作,大模型也从思维连到多智能体发展。
使用大模型做意图识别
让大模型进行意图识别也有以下两种方法:
- 使用提示词:设计特定的提示词,引导大模型生成符合预期的回答。无需要修改模型参数,而是依靠构造的输入来激发模型内部已有的知识。微调模型:使用特定的标注数据进一步训练模型,使其更好的对意图进行分类。
推理大模型
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_truthContext precision:召回的context中,与ground_truth相关的context是否排名靠前。
- context中,有多少context与ground_truth相关,以及context的排名情况。
- 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的上下文。比如:
| |
... | ... |
生成答案阶段
- 选择合适的大模型。优化提示词模板:
- 要求不编造答案。添加分隔标记。先识别问题类型,后映射到不同的提示词模板,即根据问题类型调整模板。
- 调整大模型超参:seed、presense_penalty(重复惩罚)、temperature、top_p、max_tokens
插件-Agent
智能体不仅能够与外界交互,还能处理复杂任务,几个核心模块:
- 工具模块:定义和管理工具记忆模块:
- 长期记忆:帮助智能体学习。短期记忆:临时存储当前任务信息。
- 决策、规划
- 与工具模块紧密结合,执行任务
from dashscope import Assistants, Messages, Runs, ThreadsChatAssistant = 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_infofunction_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_applicationfunction_mapper["发送请假申请"] = send_leave_application
多智能体
当机器人需要在一个请求中执行多个操作时,单个智能体可能无法有效完成所有子任务。
- planner agent
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。""")
将任务规划结果转化为列表对象,逐步解析每个步骤。
import ast# 使用Planner Agent获取任务规划planner_response = get_agent_response(planner_agent, "王五在哪个部门?帮我提交下周三请假的申请")# Planner Agent返回的是一个描述调用顺序的列表形式字符串order_stk = ast.literal_eval(planner_response)
- 工具函数summary agent
summary_agent = Assistants.create( model="qwen-plus", name='总结机器人', description='一个智能助手,根据用户的问题与参考信息,全面、完整地回答用户问题', instructions='你是一个智能助手,根据用户的问题与参考信息,全面、完整地回答用户问题')
完整流程
# 获取Agent的运行顺序agent_order = get_agent_response(planner_agent,query)order_stk = ast.literal_eval(agent_order)cur_query = query# 依次运行Agentfor 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
- 用户直观看到各个智能体的执行规则和链路。编排多个智能体的协作快速验证效果