公司动态
AI Agent原生搜索:让智能助手直接检索原始聊天记录的技术实践
1. 项目概述当你的AI助手打开聊天记录时最近在折腾AI Agent项目时我遇到了一个挺有意思的瓶颈如何让Agent高效地从海量、原始的聊天记录里找到它需要的信息这听起来像是个简单的搜索问题但实际操作起来远比想象中复杂。传统的做法是我们会把聊天记录先“结构化”——比如提取出实体、事件、情感存到向量数据库里再让Agent去检索。这就像把一堆散乱的笔记先整理成一本带目录和索引的书再让助手去查。但这个过程本身就很耗时而且“整理”的过程难免会丢失原始对话中的微妙语境和细节。“When Your Agent Opens the Chat App”这个标题精准地戳中了这个痛点。它探讨的是一种更直接、更“原生态”的方法让Agent直接面对原始的聊天日志进行搜索并且声称其效果能与经过精心设计的结构化记忆系统相媲美。这背后的核心思想是与其费尽心思去“理解”和“重构”数据不如赋予Agent强大的“阅读”和“在上下文中定位”的能力。这不仅仅是技术路线的选择更是一种设计哲学的转变——从“为记忆建模”转向“为检索赋能”。对于正在开发客服助手、个人知识管理Agent、或是团队协作分析工具的开发者来说这个话题极具吸引力。它意味着我们可能不需要构建庞大复杂的知识图谱也能让AI助手变得非常“懂”我们过去的交流。接下来我就结合自己的实践和踩过的坑来拆解一下实现这种“原生搜索”能力的关键技术、实操步骤以及那些文档里不会写的经验。2. 核心思路为什么“原始搜索”能与“结构化记忆”一战2.1 结构化记忆的得与失在深入“原始搜索”之前我们必须先理解它的对手——“结构化记忆”为何成为主流。其优势显而易见查询效率高一旦聊天记录被转化为结构化的向量或图节点基于相似度的检索如向量搜索或关系查询如图数据库查询速度极快尤其适合实时交互的Agent。语义理解强通过嵌入模型可以将用户查询的语义与存储的记忆的语义进行匹配即使字面不匹配也能找到相关内容。例如用户问“上次我们聊的那个贵一点的方案”结构化系统可能通过“价格”、“高级”、“方案”等向量找到对应的对话片段。支持复杂推理结构化的数据如将对话拆解为[发言人动作对象时间]更容易被用于逻辑推理和总结。例如Agent可以回答“上个月小明承诺过哪些事情”这类需要聚合和筛选的问题。然而其“失”也相当致命信息损耗任何结构化过程都是对原始信息的一次抽象和压缩。对话中微妙的语气、未说完的半句话、特定的梗或上下文缩写在提取实体和关系时很容易丢失。这些“噪音”往往携带关键信息。构建成本高需要设计复杂的数据管道清洗、分块、嵌入、入库不仅消耗计算资源更消耗开发与维护精力。每次架构调整都可能需要重新处理全部历史数据。灵活性差预设的结构化方案如固定的实体类型、关系定义可能无法适应所有类型的对话。当出现预料之外的查询模式时系统可能束手无策。2.2 Agent控制原始搜索的破局点“Agent-Controlled Search over Raw Chat Logs”的思路则是换了一条路走。它不预先对聊天记录做深度结构化而是将原始文本本身作为检索库并赋予Agent控制搜索过程的能力。这里的“控制”是关键它不仅仅是简单的关键词匹配。其核心破局点在于保留完整上下文搜索直接在包含完整对话轮次的原始日志上进行。这意味着Agent看到的是和人类一样的对话流包括所有的寒暄、纠正、离题讨论这些在需要精确理解意图时无比宝贵。动态上下文窗口Agent可以根据当前查询动态地决定需要“回顾”多长的上下文。例如对于“你刚才说的那句话是什么意思”这种指代性强的查询Agent可以精准定位到最近几条消息而对于“我们去年讨论过的那个项目”它可能需要在一个很长的会话中滑动一个“窗口”进行扫描和判断。搜索即推理搜索过程本身被设计为一个多步骤的推理任务。Agent可能会先对用户查询进行重写或扩展Query Reformulation然后尝试多种搜索策略如关键词、短语、语义片段并对初步结果进行相关性评估和筛选最终综合呈现。这个过程模拟了人类在聊天记录中翻找信息的行为。一个简单的类比结构化记忆就像一本编好索引的会议纪要查找已知条目很快但无法回答“那次开会时老王反驳小张时的具体语气是怎样的”这种问题。而Agent控制的原始搜索则是给了AI助手一个强大的“CtrlF”功能并且教会它如何智能地使用这个功能——不仅会搜关键词还会根据问题猜测可能的关键词变体会结合上下文判断哪段记录更相关甚至会连续翻好几页来确认信息。3. 技术架构与核心组件拆解要实现一个能有效搜索原始聊天记录的Agent我们需要搭建一个融合了传统信息检索和现代大语言模型能力的混合架构。下图展示了一个典型的系统核心流程flowchart TD A[用户查询br“上次说的预算方案”] -- B[查询理解与重写模块] B -- C[生成搜索策略br如: “Q4 预算草案 final version”] C -- D[原始聊天日志库br按会话/时间存储] D -- E{执行混合检索} E -- F[关键词/稀疏检索br快速召回] E -- G[向量/语义检索br精准匹配] F G -- H[初步结果池] H -- I[重排序与上下文聚合模块br使用LLM进行精排] I -- J[最终答案生成br“在11月5日的会议记录中...”] J -- K[返回答案与引用片段]下面我们来拆解图中的每一个关键组件。3.1 原始聊天日志的存储与预处理虽然说是“原始”搜索但完全不处理是不现实的。合理的预处理能极大提升搜索效率。存储格式 我推荐使用JSON Lines.jsonl格式按会话存储。每条记录包含完整的元数据。{ session_id: project_planning_20231105, timestamp: 2023-11-05T14:30:00Z, participants: [Alice, Bob, Charlie], messages: [ {sender: Alice, text: 关于Q4的预算草案我发群里了。, msg_id: 1}, {sender: Bob, text: 看到了第三部分成本有点高能优化吗, msg_id: 2}, {sender: Alice, text: 我看看...主要是硬件采购那块。或者我们分阶段实施, msg_id: 3} ] }为什么是JSONL因为它易于追加新会话且大多数编程语言和数据处理库都支持流式读取适合处理可能不断增长的日志。预处理关键步骤会话分割将连续的聊天流按自然中断如长时间静默、话题明显转变分割成独立的会话。这有助于缩小搜索范围。基础清洗去除纯表情符号、系统通知如“xxx已加入群聊”但保留撤回消息的提示如“xxx撤回了一条消息”因为这本身就是上下文的一部分。构建倒排索引可选但强力推荐尽管我们强调语义搜索但对“项目代号”、“特定文件名”等精确术语的查询传统倒排索引如Elasticsearch, Meilisearch的速度是无与伦比的。可以将其作为第一层快速过滤器。实操心得预处理中最容易踩的坑是过度清洗。我曾为了“整洁”删除了所有“嗯”、“哦”、“好的”这类应酬语后来发现当用户查询“我当时都答应了你怎么没做”时系统因为找不到那些表示肯定的短句无法确认当时的承诺。记住Agent需要面对的是真实的、杂乱的对话而不是清洗过的文本。3.2 查询理解与重写模块这是Agent“控制”搜索的智慧体现。用户的查询往往是模糊的、指代性的。用户问“上次说的那个事”Agent需要将其重写为“[当前用户]在[最近一周]的对话中提到的与[当前对话上下文]相关的未决事项或提议”。这个模块通常由一个轻量级LLM如GPT-3.5-Turbo, Claude Haiku驱动完成以下任务指代消解识别“我”、“你”、“他”、“那个”、“上次”等指代词的具體指代对象。时间范围推断“去年”、“月初”、“上周三”转化为具体的日期范围。查询扩展与同义词生成将“预算方案”扩展为“预算草案、财务计划、成本估算”。意图分类判断用户是想查找具体信息、总结某段时间的讨论还是追溯某个决定的形成过程。不同意图对应不同的搜索策略。# 一个简化的查询重写示例使用OpenAI API def rewrite_query_for_search(original_query, conversation_context): prompt f 你是一个智能搜索助手。请将用户的模糊查询重写为适合在聊天记录中进行检索的、明确的多组搜索关键词或短语。 考虑时间、人物、事件的具体指代。 当前对话的最后几句上下文{conversation_context[-3:]} 用户查询{original_query} 请输出一个JSON对象包含 1. rewritten_queries: 一个字符串数组包含2-3个重写后的搜索查询。 2. time_range: 推测的时间范围如[2023-10-01, 2023-11-01]或last_week。 3. key_entities: 识别出的关键实体如人名、项目名。 # 调用LLM API并解析结果 # ... return search_spec3.3 混合检索策略稀疏与稠密的结合这是核心的搜索执行层。单一检索方式很难应对所有场景必须采用混合策略。稀疏检索关键词/短语搜索工具使用像Whoosh、Elasticsearch或数据库的全文搜索功能。适用场景精确术语产品型号ABC-123、代码片段、文件名、特定人名。速度快召回精准。技巧对聊天记录按消息或小窗口如3-5条消息建立索引而不是整个会话。这样能精确定位到具体发言。稠密检索语义/向量搜索工具使用嵌入模型如text-embedding-3-small,BGE-M3和向量数据库如Chroma,Qdrant,Weaviate。适用场景概念性、意图性查询。例如“关于降低成本的想法”匹配到讨论“优化开支”、“寻找更便宜的供应商”的段落。关键点嵌入的单元很重要。将连续的对话片段如一个问答对或一个完整的观点陈述作为嵌入单元比单条消息或整个会话更有效。例如将“Q成本能优化吗 A可以看看硬件采购或者分阶段。”作为一个向量存储。混合检索流程并行执行同时用重写后的查询进行稀疏检索和稠密检索。结果合并获取两个结果集例如各Top-20然后通过重排序模型进行统一打分和排序。重排序模型如Cohere rerank,BGE reranker比用于检索的嵌入模型更精细能更好地理解查询和片段之间的相关性。3.4 结果重排序与上下文聚合检索到的是一堆文本片段可能来自不同会话的不同位置。直接扔给LLM生成答案效果会很差。我们需要先“整理”这些证据。去重与聚合合并内容高度重叠的片段。如果多个片段都指向同一核心内容只保留最完整或最相关的一个。按逻辑/时序重组如果检索到的片段属于同一个讨论线程但分散在各处可以尝试按时间顺序或逻辑关系将它们拼接起来形成一个连贯的叙事。相关性精排使用重排序模型对聚合后的候选片段进行最终打分只保留Top-5或Top-3最相关的片段作为生成答案的上下文。注意事项这个阶段最容易出现“信息失真”。聚合时一定要保留原文的发言人和时间戳。在将上下文喂给最终答案生成器时必须以清晰的格式注明来源例如[Alice, 2023-11-05]: 关于Q4的预算...。这能让最终答案更具可信度也方便追溯。4. 实战构建从零搭建一个简易聊天记录搜索Agent理论说了这么多我们来动手搭建一个最小可行产品。假设我们已有导出的微信或Slack聊天记录JSON格式。4.1 环境准备与数据加载我们使用Python主要依赖langchain用于编排、chromadb向量库、openai嵌入和LLM。# 创建环境并安装依赖 pip install langchain langchain-openai chromadb tiktoken# 1. 加载和预处理聊天数据 import json from datetime import datetime, timedelta from typing import List, Dict def load_and_chunk_chat_logs(file_path: str, chunk_size_messages: int 10) - List[Dict]: 加载聊天日志并按消息数量进行分块。 每块包含连续的若干条消息作为一个检索单元。 with open(file_path, r, encodingutf-8) as f: sessions json.load(f) # 假设是会话列表 chunks [] for session in sessions: messages session[messages] for i in range(0, len(messages), chunk_size_messages): chunk messages[i:i chunk_size_messages] # 构建块内容包含元数据 chunk_text \n.join([f{msg[sender]}: {msg[text]} for msg in chunk]) chunk_metadata { session_id: session[session_id], start_msg_id: chunk[0][msg_id], end_msg_id: chunk[-1][msg_id], timestamp: chunk[0].get(timestamp), participants: session[participants] } chunks.append({ text: chunk_text, metadata: chunk_metadata }) return chunks # 示例调用 chat_chunks load_and_chunk_chunk_logs(chat_logs_2023.json) print(f共生成 {len(chat_chunks)} 个文本块。)4.2 构建向量检索库我们将聊天块向量化并存入ChromaDB。from langchain.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings import os os.environ[OPENAI_API_KEY] your-api-key # 2. 创建向量存储 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) # 准备文本和元数据 texts [chunk[text] for chunk in chat_chunks] metadatas [chunk[metadata] for chunk in chat_chunks] vectorstore Chroma.from_texts( textstexts, embeddingembeddings, metadatasmetadatas, persist_directory./chat_memory_db # 持久化存储 ) print(向量数据库构建完成。)4.3 实现查询重写与混合检索链这里我们实现一个简单的混合检索先用向量搜索找语义相关的再用关键词在结果里二次过滤。from langchain.chains import RetrievalQA from langchain.chat_models import ChatOpenAI from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor # 3. 定义基础检索器 base_retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 10} # 先召回10个最相似的片段 ) # 4. 可选定义重排序压缩器使用LLM提取最相关部分 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) compressor LLMChainExtractor.from_llm(llm) compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverbase_retriever ) # 5. 构建一个包含查询理解的QA链 from langchain.prompts import PromptTemplate from langchain.chains import LLMChain # 查询重写链 rewrite_prompt PromptTemplate( input_variables[original_query, recent_chat], template 最近几句聊天{recent_chat} 用户当前问题{original_query} 请将用户问题重写为更适合在历史聊天记录中搜索的2个版本。考虑时间、人物和事件的指代。 直接输出两个用分号隔开的搜索查询不要额外解释。 示例用户问“上次说的预算”输出“2023年11月 预算草案;Q4 财务计划”。 ) rewrite_chain LLMChain(llmllm, promptrewrite_prompt) def intelligent_search(query, recent_context): # 步骤1: 查询重写 rewritten rewrite_chain.run(original_queryquery, recent_chatrecent_context) search_queries [q.strip() for q in rewritten.split(;)] print(f重写后的搜索查询: {search_queries}) all_docs [] for sq in search_queries: # 步骤2: 对每个重写查询进行向量检索 docs compression_retriever.get_relevant_documents(sq) # 步骤3: 简单的关键词过滤在元数据或文本中 # 这里可以加入更复杂的关键词匹配逻辑 filtered_docs [doc for doc in docs if any(keyword.lower() in doc.page_content.lower() for keyword in query.split())] all_docs.extend(filtered_docs) # 步骤4: 去重根据内容或元数据 seen set() unique_docs [] for doc in all_docs: identifier (doc.page_content[:100], doc.metadata.get(session_id)) if identifier not in seen: seen.add(identifier) unique_docs.append(doc) # 步骤5: 按相关性排序这里简单使用原始检索分数生产环境应用重排序模型 unique_docs.sort(keylambda x: x.metadata.get(score, 0), reverseTrue) return unique_docs[:5] # 返回Top-5结果 # 6. 最终答案生成 def generate_answer(query, retrieved_docs): context \n---\n.join([f来自会话 {doc.metadata.get(session_id)}:\n{doc.page_content} for doc in retrieved_docs]) answer_prompt PromptTemplate( input_variables[context, question], template 你是一个助手需要根据以下从聊天记录中检索到的上下文来回答问题。 如果上下文中有足够的信息请基于它给出精确答案并引用来源如会话ID。 如果信息不足请如实说明不要编造。 上下文 {context} 问题{question} 答案 ) answer_chain LLMChain(llmllm, promptanswer_prompt) answer answer_chain.run(contextcontext, questionquery) return answer # 模拟使用 recent_chat 用户我们接下来干嘛\n助手我正在检查之前的讨论。 user_query 上次定的项目截止日期是什么时候 retrieved intelligent_search(user_query, recent_chat) final_answer generate_answer(user_query, retrieved) print(检索到的片段:, [doc.metadata for doc in retrieved]) print(最终答案:, final_answer)4.4 效果评估与迭代搭建完原型如何判断它是否“匹敌结构化记忆”你需要一个评估集。构建测试集从聊天记录中手动挑选20-50个有代表性的问题并标注标准答案或相关片段。定义评估指标召回率系统找到的相关片段占所有相关片段的比例。精确率返回的片段中真正相关的比例。答案准确性最终生成的答案与标准答案的匹配程度可以用LLM辅助评判。A/B测试将你的“原始搜索”Agent与一个基于结构化记忆如预先提取所有任务和日期存到数据库的基线系统进行对比。看看在复杂、指代性强的查询上你的系统是否有优势。5. 避坑指南与性能优化在实际部署中你会遇到各种预料之外的问题。以下是我踩过的一些坑和解决方案。5.1 常见问题与排查问题检索速度慢尤其当聊天记录超过10万条时。排查检查向量化的单元是否过大。将整个会话嵌入成一个向量是灾难性的。尝试更小的块如3-5条消息。解决分层索引先按时间如按月或会话类型建立顶层索引快速缩小范围。使用更快的嵌入模型text-embedding-3-small在速度和效果上取得了很好的平衡。对于中文BGE-M3或voyage-2也是优秀选择。引入稀疏检索作为前置过滤器先用关键词快速筛选出可能相关的会话比如包含“预算”、“日期”等再在这些会话的片段上进行向量搜索。问题Agent总是找到不相关的片段或者“幻觉”出不存在的内容。排查问题通常出在“查询重写”或“结果重排序”环节。重写后的查询可能偏离原意或者重排序模型不够精准。解决强化查询重写在重写提示词中更严格地约束LLM要求它必须基于最近上下文进行指代消解。可以尝试少样本few-shot提示提供几个优秀的重写例子。引入交叉编码器重排序不要只用向量相似度排序。使用像BGE-Reranker这样的专用重排序模型它对查询-文档对进行深度交互计算相关性判断准确得多。设置置信度阈值对于重排序后得分过低的片段直接过滤掉不提供给答案生成器。宁可回答“没找到”也不要提供错误信息。问题处理长对话时上下文窗口不够用。排查单个聊天块可能很长或者检索到的多个块总长度超过了LLM的上下文限制。解决动态上下文选择不是把所有检索到的块都塞进去。让LLM或一个更小的模型先快速浏览每个块的摘要选择最可能包含答案的1-2个完整块。摘要链对于非常长的相关块先使用LLM生成一个简洁的摘要再将摘要和最关键的原句一起送入最终答案生成环节。5.2 高级优化技巧元数据过滤充分利用聊天记录的元数据时间、参与者、会话类型。在检索时允许用户或Agent主动添加过滤器如“只搜索去年我和小王的对话”。这能极大提升精准度。会话图构建虽然我们不做全量结构化但可以构建一个轻量的“会话关系图”。记录哪些会话讨论了相似话题通过主题模型或会话嵌入聚类当在当前会话中搜索不到时可以跳转到相关会话去查找。这模拟了人类的联想记忆。持续学习与反馈记录用户的每次搜索和点击或对答案的“赞/踩”。用这些反馈数据微调你的重写模型或重排序模型让系统越来越懂你和你的团队的语言习惯。混合记忆系统不必非此即彼。对于高度结构化、频繁查询的信息如“项目的最终截止日期是X月X日”可以在系统确认后主动将其提取到一张结构化的“事实表”中。形成“原始日志搜索”为主“结构化事实快查”为辅的混合体系。6. 总结与展望让Agent直接搜索原始聊天记录不是一个偷懒的替代方案而是一种尊重数据复杂性和需求多样性的务实选择。它放弃了预先定义所有结构的野心转而追求检索时的灵活性与智能。从我的实践来看在应对开放域、充满指代和上下文的查询时这种方法的优势非常明显用户体验更接近与一个真正“读过”所有聊天记录的人类助手交流。当然它也对我们的工程实现提出了更高要求我们需要设计更智能的查询理解模块需要混合多种检索技术需要精心设计重排序和结果聚合的逻辑。这其中的每一个环节都有大量的调优空间。最后分享一个我个人的深刻体会在构建Agent记忆系统时最重要的不是追求技术的先进性而是理解记忆被使用的场景。如果你的Agent主要回答的是明确的事实性问题如“客户的电话号码是多少”那么一个简单的键值对数据库可能就够了。但如果你的Agent需要理解讨论的脉络、捕捉情绪的波动、追溯一个想法的演变那么赋予它“翻阅原始记录”的能力可能就是通往更强大智能体的关键一步。这条路还在不断演进但已经能看到当Agent真正“打开”聊天应用时它能看到的是一个远比结构化数据库更丰富、更鲜活的世界。