尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
LangGraph+Elasticsearch:构建可控可查的人机交互Agent
把大模型接进业务系统时最常见的坑往往不是模型本身不够聪明而是整个智能体缺少“结构”聊着聊着就忘了用户前面到底说了什么让它去查内部库又不知道怎么查明明该调用工具却硬生生编一个答案出来。要解决这些问题我在实践里选择把 LangGraph 和 Elasticsearch 组合在一起LangGraph 负责把人机交互 Agent 的对话、决策、工具执行编排成一张可回滚、可复盘的状态图Elasticsearch 则承担记忆和知识底座给 Agent 提供关键词与向量混合检索的能力。这套方案我最近在一个企业知识助手类项目里完整跑通过下面把设计思路、关键代码和踩坑记录都摊开讲。1. 为什么是 LangGraph 加 Elasticsearch1.1 人机交互 Agent 的本质不是聊天先说清楚一个观点真正的“人机交互 Agents”不是套一个问答界面就完事。用户给出一句话Agent 可能要经历理解意图、拆解任务、查资料、调接口、整理结果、再回到对话里这一整条链路。比如我常举的一个例子用户问“帮我找一下上个月所有状态为进行中的项目把负责人和截止时间汇总成表格”。这里面至少包含两层工作一是理解“上个月”“进行中”这些语义条件二是真实地去一个项目管理库里面执行检索并把结果结构化返回。如果没有工具调用能力纯靠大模型的知识边界去猜结果一定惨不忍睹。所以 Agent 的第一件事是“有工具”。第二件事是“有状态”。状态不是简单存几轮对话而是要把检索结果、中间结论、已经确认过的条件都记录下来。第三件事是“有记忆”跨会话记住用户偏好和历史决策。这也正是 LangGraph 的切入点。它是一个面向 Agent 的图式编排框架节点表示动作边表示流转条件整体像一张有向图甚至带环的图大模型决定要调用工具就走到工具节点工具执行完再回到模型节点继续推理任务完成才退出循环。这种“模型驱动、图编排”的形态天然适合处理多轮工具交互。1.2 两者怎么分工编排框架与知识底座LangGraph 负责“怎么走”Elasticsearch 负责“往哪查、记住什么”。我在初版原型里其实试过直接把 Elasticsearch 当成普通数据库所有信息都封装成工具丢给 Agent 调。后来发现这样搞太糙原因有两点一是 Agent 的意图往往不是精确查询需要语义匹配单纯用关键词查会漏二是对话产生的历史信息散落在各个会话里整理和召回的成本很高。后来我把 Elasticsearch 的角色重新定义成三层第一层是知识的检索工具也就是 RAG 的标准做法给 Agent 提供文档检索能力。第二层是长期记忆存储把每段会话的摘要、用户偏好、决策记录存成带向量的文档下次会话时按语义相似度召回。第三层是业务数据查询入口例如查订单状态、查流程审批情况只要数据已经在 ES 里Agent 就能直接触发查询。LangGraph 则负责把这些能力编排成一条条可执行的路径。搜索只做一次还是做多次、结果不满意要不要二次改写查询、高风险操作之前要不要暂停找用户确认这些逻辑全部落在状态图里。两者配合的典型效果是Agent 先通过 Elasticsearch 快速拿到候选资料再交给大模型判断和整理如果信息不够模型会再次触发同一工具调整关键词或过滤条件继续检索直到自己认为信息足够为止。整个过程被 LangGraph 可视化地记录下来。1.3 这套方案适合哪些场景从我这几个月的实践看最有价值的落地场景集中在四个方向企业内部知识问答比如规章制度、技术文档、项目档案的检索问答。数据运营助手用户不直接写查询语句而是用自然语言描述条件Agent 负责拼查询、执行并返回结果。客服质检和工单处理Agent 需要同时检索历史工单和知识库再给出处理建议。个人助理类应用需要跨会话记忆用户的日程、偏好、常用设置。在这些场景里单纯的大模型对话都会面临同样的问题知识不够新、上下文装不下、用户习惯记不住。Elasticsearch 的混合检索和向量记忆恰好能把这三块短板补上LangGraph 再把补位过程组织得像一个“有逻辑的人”在工作。2. 核心概念状态图、工具调用与 Elasticsearch 的三重角色2.1 LangGraph 状态图到底在编排什么不少人刚开始接触 LangGraph 会把它理解成一个流程引擎像画流程图一样把节点连起来。这个理解对了一半。它真正厉害的地方在于“状态”这个概念。在 LangGraph 里状态就是整个会话的公共黑板。每个节点从黑板里读数据干完活再把结果写回黑板。节点之间不直接传参而是通过状态共享数据。这样做的好处非常明显任何一步出错你都能从状态里看到是哪一步写入的数据不对任何一步重跑也不会因为依赖关系漏掉上下文。实际定义状态的方式很简单用 TypedDict 描述字段就行from typing import TypedDict, Annotated, operator class AgentState(TypedDict): messages: Annotated[list, operator.add] retrieved_docs: list tool_results: dict memories: list这里messages用了Annotatedoperator.add意思是多个节点返回的 messages 会合并进同一个列表而不是互相覆盖。这是 LangGraph 里非常实用的一个语义对话模型节点追加一轮消息工具节点追加工具返回结果最终消息历史被完整保留。节点就是普通的 Python 函数输入是当前状态输出是一个字典字典里只写你希望变更的字段。边则决定节点之间的流转。除固定边之外LangGraph 还提供了条件边典型用法是def route_after_model(state): last_message state[messages][-1] if last_message.tool_calls: return tools return end graph.add_conditional_edges(chat, route_after_model)这段代码的核心含义是大模型决定调用工具就进入工具节点否则直接结束。工具执行完再回到 chat 节点形成循环直到模型认为任务完成。2.2 Elasticsearch 在 Agent 里的三重身份我在前面的项目方案里Elasticsearch 不是单纯挂着给 Agent 调用的一个搜索接口而是三种身份同时并存。三种身份对应三种不同的数据模型和使用方式。第一重身份知识库。这是 RAG 的标准做法。文档写入 ES内容字段做全文索引内容向量字段做余弦相似度检索。搜索的时候同时跑关键词查询和向量查询再用 RRF 把两份结果融合排序。关键词负责精确匹配实体向量负责语义近义互补效果比单用一种好很多。第二重身份长期记忆。每轮会话结束把用户的核心需求、已确认的信息、决策结论压缩成一段摘要连同向量的形式写入一个独立的记忆索引。新会话开始时Agent 先用用户当前输入的向量去召回若干条历史摘要放进 system prompt。这样即使用户隔了三天再次访问Agent 也记得上次聊到哪、结论是什么。第三重身份业务查询工具。比如用户说“查一下编号 P2024-018 的项目状态”Agent 不会自己去猜而是调用一个封装好的 ES 查询工具按业务字段精确检索。这里用到的往往是 keyword 字段和 term 查询走的是结构化检索路线。这张表可以直观地看到三重身份的区别身份数据模型典型查询方式面向问题知识库text dense_vectormatch knn RRF语义找文档长期记忆text dense_vector metadataknn filter跨会话记忆业务查询keyword textterm range bool结构化精确查询实践下来三种身份最好拆成三个索引不要混在一个索引里。否则业务字段过滤和向量检索的字段映射会互相干扰调 mapping 的时候非常痛苦。3. 实操搭一个带混合检索和长期记忆的 Agent3.1 环境准备依赖与索引设计先说依赖我用的核心库是langgraph和elasticsearch。前者负责图编排后者负责搜索和存储。为了让向量化过程可自控我接了一个独立的嵌入模型接口没有把它耦合进 Agent 的逻辑里。索引设计是第一个重要步骤也是后面最容易返工的地方。知识库索引的 mapping 大致如下{ mappings: { properties: { content: { type: text }, content_vector: { type: dense_vector, dims: 768, index: true, similarity: cosine }, source: { type: keyword }, create_time: { type: date } } } }这里有几个关键点dims必须和嵌入模型输出的维度完全一致差了哪怕一位写入文档时就会被 ES 拒绝similarity设成cosine是为了配合向量检索时按余弦距离打分source用 keyword 类型是为了后续做过滤条件比如只检索某个知识分类下的文档。3.2 把 Elasticsearch 封装成 Agent 的两把武器我习惯把 ES 操作封装成两个 LangChain 风格的函数一个做知识检索一个做记忆写入。注意这里没有使用任何平台特定对象就是普通的 Python 函数LangGraph 会把它们注册成可被模型调用的工具。知识检索函数是核心它做的是混合检索。核心思路是同时执行关键词 match 和向量 knn最后用 RRF 融合。我用的是新版 ES 的 RRF 参数from elasticsearch import Elasticsearch es Elasticsearch(http://localhost:9200) def hybrid_search(query_text: str, query_vector: list, top_k: int 5): body { size: top_k, query: { bool: { should: [ { match: { content: { query: query_text, boost: 0.3 } } }, { knn: { field: content_vector, query_vector: query_vector, num_candidates: 50, k: top_k } } ] } }, rank: {rrf: {}} } resp es.search(indexknowledge_index, bodybody) return [ { content: hit[_source][content], score: hit[_score] } for hit in resp[hits][hits] ]这里boost是用来控制关键词分数权重的。我建议关键词权重不要太高因为用户在大模型对话里提出的问题往往是口语化的关键词重合率低纯靠关键词很难召回语义向量才是主要路径。RRF 的作用是融合两种排序结果不直接叠加原始分数而是基于排名融合稳定性好很多。记忆写入函数稍微简单一点本质就是往记忆索引里写一条文档def save_memory(text: str, session_id: str, user_id: str, vector: list): doc { content: text, content_vector: vector, session_id: session_id, user_id: user_id, create_time: now } es.index(indexmemory_index, documentdoc)这一条函数看起来简单但它是跨会话记忆的地基。每次对话收敛出结论后都会调用一次长期积累下来Agent 对每个用户的了解就会越来越具体。3.3 组装 StateGraph 并跑通整个循环图的结构我设计得比较标准一个 chat 节点负责和大模型交互一个 tools 节点负责执行工具然后用条件边把两者连起来形成循环。from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolsNode tools [hybrid_search, save_memory] def chat_node(state): # 把记忆和检索结果拼进 prompt然后调用大模型接口 response llm_with_tools.invoke(state[messages]) return {messages: [response]} graph StateGraph(AgentState) graph.add_node(chat, chat_node) graph.add_node(tools, ToolsNode(tools)) graph.set_entry_point(chat) graph.add_conditional_edges( chat, route_after_model, {tools: tools, end: END} ) graph.add_edge(tools, chat) app graph.compile()这里每轮执行的流程是用户输入进入 chat 节点大模型判断当前信息是否足够如果不够返回 tool_calls条件边把流程导向 tools 节点工具执行完把结果追加到状态里再回到 chat 节点让模型重新判断直到模型不再请求调用工具直接输出最终答案。这个循环跑通后你就拥有一个真正“会主动查资料”的 Agent 了。我实际测试中最直观的感受是用户问了一个模糊问题Agent 第一次检索结果太泛它会把问题拆得更细加关键词、加过滤条件第二次检索明显更精准。这种“自我修正”能力就是状态图带来的。3.4 长期记忆的写入与召回长期记忆要解决的核心问题是如何让 Agent 记得一个老用户。传统做法是把所有历史消息一股脑塞进 prompt但 token 成本高而且无关历史太多反而干扰回答。我的做法是会话结束时把本轮对话的核心结论压缩成 200 字以内的摘要。对这个摘要做向量化连同用户 ID、会话 ID 写入记忆索引。下一次该用户发起对话时先把用户当前问题向量化在记忆索引里做 kNN 检索取回最相关的若干条摘要。将召回的摘要拼进 system prompt让 Agent 知道用户之前聊过什么。召回部分我习惯加上用户 ID 过滤避免 A 用户的记忆被召回给 B 用户def recall_memories(user_id: str, query_vector: list, top_k: int 3): body { size: top_k, query: { bool: { filter: [ {term: {user_id: user_id}} ], must: [ { knn: { field: content_vector, query_vector: query_vector, num_candidates: 20, k: top_k } } ] } } } resp es.search(indexmemory_index, bodybody) return [hit[_source][content] for hit in resp[hits][hits]]这里用 filter 条件精确过滤用户用 knn 做语义排序两个层次的检索各司其职。踩过一次坑后我特别提醒自己记忆索引里的向量和知识库向量如果来自不同模型召回效果会非常差所以两个索引必须共用同一套嵌入模型。4. 人机交互的关键设计确认、流式与状态回滚4.1 高风险动作前插入人工确认Agent 具备工具调用能力后最让我警惕的是“自动执行一切”。比如用户说“把所有已完成的工单批量归档”Agent 如果直接把批量写操作发出去一旦条件判断有误后果是灾难性的。LangGraph 处理这个问题的思路非常优雅在工具节点前设置一个中断点让流程停下来把控制权交还给人类。只有用户确认流程才继续往下走。app graph.compile( checkpointermemory_checkpointer, interrupt_before[execute_batch_action] )这里interrupt_before指定了在某个节点执行前中断。前端拿到暂停信号后把待执行的参数展示给用户用户点“确认”后再用同一个线程 ID 恢复执行。这个机制让人机交互里最关键的“许可”环节有了明确的技术抓手。我在实际项目中还有一个额外的保险把高风险工具单独放进一组dangerous_tools只有显式经过确认节点才允许注册进工具的可用列表。这样模型即便被诱导去调用也会因为没有注册而无法真正执行。4.2 流式输出让交互更接近人人机交互体验里最影响感知的其实是响应延迟。如果 Agent 要先检索、再思考、还要调一次工具整个过程可能要好几秒。如果前端一直等待用户很容易以为系统卡死了。LangGraph 的编译结果可以直接使用流式模式把节点执行过程和消息生成过程逐步推给前端。实现上大概是这样async for event in app.astream_events( {messages: [user_input]}, configconfig, versionv2 ): if event[event] on_chat_model_stream: token event[data].get(chunk, ) await websocket.send(token)这样用户在模型生成第一个 token 时就能看到文字一点点出现中间节点切换时也能收到“正在检索知识库”“正在整理结果”这类过程提示。按我的经验流式输出带来的体验提升甚至比模型能力提升还明显。4.3 多轮、并发会话的状态隔离一个人机交互系统常态是同时存在几百上千个会话。LangGraph 用一个叫检查点的组件保存每一步状态配合线程 ID 做隔离。config {configurable: {thread_id: session_20241008_01}}同一条线程 ID 下的多次请求会共享状态相当于同一个会话继续往下聊。换成新的线程 ID就是一个全新的、不携带历史状态的会话。这个设计让我在处理并发时非常省心只要入口把用户会话 ID 映射成 LangGraph 线程 ID天然就避免了不同用户之间的状态互相污染。状态回滚也很自然检查点保存了每一步的完整状态如果发现某次工具执行引入了错误数据可以直接把线程状态恢复到执行前的检查点重新走流程不需要手动清理一堆中间变量。5. 常见问题与排查速查表5.1 向量检索没结果这是我被问得最多的一个问题。明明索引里有文档但 kNN 查询就是返回空。原因一般是三种一是dims和嵌入模型输出维度不一致写入时就被拒绝了建议先检查索引 mapping二是索引里文档确实存在但content_vector字段为空可能是写入链路里向量化步骤没跑成功去查写入日志三是 num_candidates 设置太小导致候选集不够建议至少设为 k 的 5 到 10 倍。排查时我习惯先做最小验证不依赖 Agent直接用 Python 客户端手动发一次 kNN 查询如果这一步都不通那就不是 Agent 的问题而是 ES 索引或者向量化服务的问题。5.2 对话历史像失忆表现是用户在同一会话里问了第二句Agent 完全不知道第一句说了什么。这个现象多半是检查点没有生效。在 LangGraph 里只要编译时没有传 checkpointer每次请求都会使用全新的状态所以多轮记忆必然失效。另一个常见原因是线程 ID 没有正确传递前后请求落到了不同线程自然互不相识。排查思路是先看请求日志里两次调用的 thread_id 是否一致再看编译时是否传了 checkpointer。5.3 检索结果相关性差混合检索效果不好的时候先不要急着换模型而是检查 RRF 融合和权重。我在项目中用过几个不同的检索策略最终的结论是关键词和向量最好都用但方式要看场景。技术文档类知识关键词命中本身就有意义向量负责泛化而用户问题口语化严重时关键词召回质量差主要依赖向量。两种场景对应不同的 boost 参数前者关键词给 0.5后者给 0.1。另外 RRF 对不同 ES 版本的语法有差异我遇到的坑是刚开始用老版本的写法直接报错后来换成了新版rank参数才稳定。关于不断兼容的复杂点建议升级 ES 版本后先跑一次最小混合检索用例验证。5.4 上下文爆炸与性能瓶颈每轮检索都往状态里塞大量文档跑几轮后上下文会越来越大最终要么 token 超限要么模型注意力被无关内容稀释。我习惯在写入状态前对文档做剪裁只保留标题、摘要和关键段落大幅截断正文同时限制 top_k一般知识检索只取 3 到 5 条记忆召回 2 到 3 条。有条件的话还可以对同一会话内的重复检索结果做缓存避免同一问题被反复查询 ES。性能方面向量检索本质上是内存密集型操作。项目初期的数据量几百条文档没什么感觉到了几十万条就必须重视索引分片规划和节点内存。给 dense_vector 字段设index: true会在内存里建立 HNSW 图数据量越大内存占用越明显。6. 影响范围与扩展方向6.1 这套架构适合落在哪些业务回到人机交互这个角度看这套架构真正改变的不只是问答的准确率而是系统的可解释性和可控性。以前做一个对话机器人回答错了你只能看到一句错误的文本不知道它为什么这么说。现在有了 LangGraph 的状态图和检查点每一步调用了什么工具、检索到了什么文档、模型基于哪些上下文做了判断全部可以复盘。这种可解释性对企业的客服合规、工单管理、审计追溯来说价值甚至超过了准确率本身。Elasticsearch 的加入则让 Agent 从“只懂语言模型知识”的封闭体变成一个可以连接企业内部数据资产的中枢。无论数据是结构化报表还是非结构化文档只要灌进去Agent 就能在对话中按需取用。从我接触过的业务反馈看最适合优先落地的不是那些追求“全自动”的场景而是“辅助决策”类场景人工客服结合 Agent 检索知识库运营人员用自然语言从数据平台拿报表法务人员让 Agent 快速整理历史合同条款。这些场景风险低、收益明显、上线周期短。6.2 再往上走多智能体、人工审批与认知监控如果这套单 Agent 架构跑稳定了我会建议往三个方向扩展。第一个方向是多智能体协作。LangGraph 支持把多个图嵌套成子图可以把检索 Agent、业务查询 Agent、汇总 Agent 拆开各管一段再由主 Agent 协调。好处是每个子图的状态更简单定位问题更快。第二个方向是人工审批流的完善。当前只在高风险工具前插入中断还不够。完善的做法是把审批记录也写进 Elasticsearch作为一份审计日志。将来你甚至可以做一个专门的分析面板查某类操作的审批通过率、平均耗时回头再优化交互流程。第三个方向是认知监控。把每次会话的状态快照、工具调用序列、检索文档列表同步到监控索引里。一旦出现用户满意度下降你就可以回放整个过程找到是哪一步检索跑了偏或者哪一次工具调用返回了脏数据。这样的监控会让 Agent 系统的迭代速度提高非常多因为你不再靠用户投诉去发现问题。最后分享一个我这几次实操下来最深的体会。搭好一套 Agent 架构并不难难的是让它稳定地跑在真实业务里。LangGraph 和 Elasticsearch 这套组合一个解决“可控”一个解决“可查”合在一起恰好把人机交互这个主题最核心的两个底座立住了。如果你也正准备接一个类似的知识助手或自动化服务我建议先把第一版跑通再一步步加记忆、加确认、加监控。功能不在多状态清晰、链路可控、数据可查这三条做到了系统自然会越来越顺手。
RELATED

相关推荐

用C#和ASP.NET Core在浏览器中查询Access数据库的轻量方案

用C#和ASP.NET Core在浏览器中查询Access数据库的轻量方案

简介:这份资源面向具备一定C#基础的开发者与小型项目维护人员,解决如何通过Web页面查询Access数据库的实际问题。内容围绕ASP.NET与ADO.NET技术栈展开,涵盖OleDbConnection连接建立、OleDbCommand执行SQL、OleDbDataReader读取结果&#xff0…

📅 2026/10/20 3:34:23
AI批量汉化外文说明书,把“不会用”退货成本压下去

AI批量汉化外文说明书,把“不会用”退货成本压下去

做电商或硬件产品的朋友应该都遇到过这种后台场景:退货订单里整整齐齐排着一串备注,写的是“不会用”“装不上”“看不懂说明书”。点开聊天记录才发现,产品本身没毛病,纯粹是用户卡在了“理解”这一步。尤其做跨境或外贸供应链的…

📅 2026/10/20 3:29:22
5800张网球检测数据集:从标注设计到模型训练落地全解析

5800张网球检测数据集:从标注设计到模型训练落地全解析

做体育视觉AI这一年多,最能让我头疼的不是模型结构怎么搭,反而是数据。尤其网球这种高动态、小球体、强遮挡的赛道,想找一个能直接拿来训练出可靠检测器的数据集太少了。网上流传的那几份公开数据,要么分辨率低,要么场…

📅 2026/10/20 3:29:22
MORE NEWS

更多资讯

📰

人脸灰度图像彩色复原:从原理到工程实践的深度学习方案

你有没有翻到过家里的老照片?黑白照片里,爷爷的头发、外婆的裙子、街角那扇门是什么颜色,往往只能靠想象。我一直觉得,给灰度图像做彩色复原是一件"看着简单、做起来极麻烦"的事——它不是填色游戏,而是一个…

📰

野生保护动物识别系统实战:基于ResNet与Django的图像分类部署全流程

从野外红外相机攒下来的一堆动物照片需要人工一张张翻着看,到能直接在网页上拖一张图进去就返回"雪豹、置信度0.94",这条路我走了差不多一年。做完这套野生保护动物识别系统之后最大的体会是:深度学习模型真正难产的地方不在网络结…

📰

CIFAR-10图像分类实战:从基础CNN到96%准确率的深度学习工程指南

1. 这个项目到底在做什么CIFAR-10 这个数据集在图像分类圈子里算是“老熟人”了。它包含 10 个类别、共 6 万张 3232 的彩色小图,其中 5 万张训练、1 万张测试。类别分别是飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。每类 6000 张,分布非常均衡&a…

📰

快速排序核心原理与工程优化实战:分区、基准选择与避坑指南

很多人学完快速排序,第一反应是“代码背下来了,但让我自己写一遍却总觉得哪里不对,尤其是分区那一步,稍微改动一个边界条件,数组就越界或者死循环”。还有人问得最多的是:“网上快排版本这么多,…

📰

rea三步法:把读过变成用得上,告别收藏夹焦虑

事情要从我浏览器书签栏里那七千多条收藏说起。某天想找一篇半年前读过的关于分布式事务的文章,翻了三个子目录,点坏了不知道多少下滚轮,最后打开的那篇,标题眼熟、内容陌生——我明明把它存了下来,却等于从没见过。那…

📰

AI应用测试全攻略:五大核心维度与实战避坑指南

做了好几年AI应用的交付,我最大的感受就是:AI应用测试不是“上线前补一个环节”,而是一条独立的、需要提前设计的工程主线。很多团队在模型效果上卷得很凶,但对测试的投入往往停留在“跑几个用例看看回答对不对”,结果…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬