让向量听懂孤独:我是如何用 Elastic Agent Builder 构建一个情绪疗愈 agent 的

发布时间:2026/7/17 21:14:00
让向量听懂孤独:我是如何用 Elastic Agent Builder 构建一个情绪疗愈 agent 的 作者翟献帅| Elasticsearch Agent Builder Hackathon ·北京本内容遵循CC 4.0 BY-SA版权协议深夜的聊天框里有人打下这样一句话“最近觉得再热闹也和谁都隔着一层提不起劲。”这句话里没有“孤独”两个字。没有“抑郁”没有“难过”没有任何一个情绪词典里的词。任何基于关键词的系统都会漏掉它。而这恰恰是问题所在人在表达孤独的时候几乎从不使用“孤独”这个词。他们说的是 “隔着一层”是 “提不起劲”是 “忙到麻木”是“拥有的不少却不知道为了什么”。情绪的表达天然是隐喻的、迂回的、绕着走的——这也是为什么在“情绪”这件事上关键词匹配从一开始就注定失败。7 月 11 日我参加了 Elastic 与阿里云在北京举办的 Agent Builder Hacknight。四个小时从零交付一个可演示的原型。我没有花时间去找题目 —— 我想做的东西已经在我脑子里放了很久一个能真正 “听懂” 这种话的陪伴 agent。最后它拿了二等奖。但比名次更有价值的是这四个小时里我踩过的坑、以及一个我完全没有预料到的结论。为什么“听懂孤独”很难我们身处一个史上最 “连接” 的时代——消息秒回、社交不断。可越来越多的人在最热闹的房间里依然觉得没有人真的懂自己。现有的方案大致分两类。一类是专业心理服务它回答的是 “你有没有病”另一类是各种情绪打卡、心情记录 App它给你一个标签今天你是 “开心” 还是 “难过”。但很多人真正想问的其实是第三个问题“有没有人愿意先听我说。”而从技术角度看难点非常具体这类表达的语义几乎完全藏在字面之下。“再热闹也隔着一层”和“机械重复里的倦怠”之间没有任何一个共同的关键词但它们说的是同一种感受。要接住这句话系统必须理解的是句子的“言外之意”而不是它的词。这正好是向量语义检索最擅长、也最该被用在的地方。系统一只狐狸和六颗“孤独星球”我在 Elastic Cloud ServerlessElasticsearch 9.3和 Agent Builder 上构建了这个系统灵感来自《小王子》里“驯服”的主题 —— 狐狸说正因为你为你的玫瑰花费了时间她才对你独一无二。产品的样子是这样的你向一只叫“狐狸”的 agent 倾诉它会带你降落到一颗最贴合你此刻心境的“孤独星球”用那颗星球居民的故事映照你的感受并且 —— 它会记得你。标题一整个系统由三层组成。六颗星球把心理状态翻译成故事我把六种现代人的孤独做成了六个原型点灯人 —— 重复性倦怠日复一日、忙到麻木热闹里也孤独国王 —— 掌控焦虑什么都想抓在手里越用力越累爱慕虚荣者 —— 渴望被看见拼命表现掌声散去却没人真懂商人 —— 意义感缺失拥有很多却不知道为了什么地理学家 —— 旁观者疏离什么都看在眼里却总站在生活外面狐狸的麦田 —— 渴望被驯服想靠近又怕受伤每颗星球在 Elasticsearch 里是一条文档包含它的情绪特征、映照文字和引导方向。用户的一句倾诉进来先被向量化再和这六颗星球做检索命中最贴合的那一颗。图1 狐狸根据我的情绪将我分配到了点灯人星球狐狸agent用内置工具编排而不是造轮子狐狸是一个在 Agent Builder 里创建的自定义 agent推理模型用Elastic 内置的 Anthropic Claude Sonnet 4.5太方便了特别丝滑。这里有一个我一开始想错的地方我原本打算写一堆自定义工具 —— match_planet、find_healing_wisdom、get_bond。后来我发现Agent Builder 内置的 platform.core.search 和 ES|QL 工具generate_esql / execute_esql已经完全够用了。我要做的只是在 agent 的 instructions 里明确告诉它有哪几个索引、字段是什么、什么时候该查哪个。对于一个四小时的 hackathon 来说这个决定救了我至少一个小时。而且事后看它也是对的 —— 我的语料规模很小六颗星球、三十条疗愈语料与其造一堆工具不如把精力花在检索质量和体验上。驯服指数把“被记住”变成可以计算的东西《小王子》里“驯服”的内核是建立关系需要时间。我想让这件事在产品里可被感知于是用 ES|QL 对用户的情绪日志做时序聚合算出一个 “驯服指数”FROM emotion-journal | WHERE user_id ?user_id | STATS visits COUNT(*), avg_intensity ROUND(AVG(intensity), 1), span_days ROUND((MAX(timestamp) - MIN(timestamp)) / 86400000, 0) | EVAL taming_index ROUND(LEAST(100, visits * 8 span_days * 1.5), 0)陪伴的次数、跨越的天数合成一个 0–100 的关系深度。在我这个月经过一系列与产品对话后当我说 “我是 lin又来了”狐狸会回答“这是你这个月第 16次来你常降落在狐狸的麦田的星球……。”这不是写死的台词是从一个月的真实情绪时序里算出来的。图2 狐狸根据真实情绪时序计算效果为什么我用“六种原型”而不是情绪分类最初我考虑过一个更常规的方案做情绪分类。开心、难过、焦虑、愤怒 —— 给用户的每句话打一个标签。但它在真实场景里立刻失效了原因很简单标签不疗愈人。你告诉一个深夜睡不着的人“检测到你的情绪是焦虑”这句话对他没有任何意义 —— 他当然知道自己焦虑。而且在情绪场景里“被诊断”会让人竖起防御。一个正在孤独的人最怕的就是被当成一个待分类的样本。而 “你让我想起点灯人 —— 他从不停下可有没有人问过他累不累”这句话是完全不同的东西。它不是标签是一面镜子。六原型的设计让 agent 能做三件分类做不到的事给出一个具体的、可共情的映照给出一个针对那种孤独的引导方向以及让用户在“我不是唯一一个这样的人”里得到安慰。这些原型的具体刻画在真实使用中还需要迭代 —— 我现在也不敢说这六种就是最好的切分。但方向是对的在情绪这件事上共情的分辨率比分类的准确率更重要。Elasticsearch帮我实现的部分整个系统里我花在“语料怎么写”和“检索怎么配”上的时间比其他任何部分都多。这也是回报最高的投入。嵌入内置多语言模型零外部服务我的内容全是中文所以选了 Elastic 内置的多语言嵌入模型.multilingual-e5-small通过 EIS 托管PUT _inference/text_embedding/e5-multi { service: elasticsearch, service_settings: { model_id: .multilingual-e5-small, adaptive_allocations: { enabled: true, ... } } }这个选择的好处是不需要任何外部 embedding 服务、不需要额外的 API key、不需要运维。对个人参赛者来说少一个依赖就是少一个失败点。这里有个坑值得说我第一次创建这个端点时收到了 502 Bad Gateway以为失败了。其实模型在后台还在下载端点已经建好了——GET 一下就能看到。另外我的配置里 min_number_of_allocations 是 0空闲时缩容省资源这意味着第一次调用会有冷启动可能要等十几秒甚至超时。演示前先“预热”一次这个问题就不存在了。索引semantic_text加一个BM25兄弟字段检索字段用 semantic_text自动切块 向量化同时保留一个普通 text 字段做 BM25signature_text: { type: text }, signature_semantic: { type: semantic_text, inference_id: e5-multi }两个字段写同一段文字。这样我就能在同一个语料上同时做向量检索和关键词检索。混合检索向量捕捉言外之意BM25兜底最终的检索用 RRF 把两路融合POST planet-archetypes/_search { retriever: { rrf: { retrievers: [ { standard: { query: { match: { signature_text: ... } } } }, { standard: { query: { semantic: { field: signature_semantic, query: ... } } } } ], rank_window_size: 20 } } }向量负责“再热闹也隔着一层”这种言外之意BM25 负责那些明确说出来的词。RRF 把两者的排序融合起来。我还做了一件事事后证明特别值得写了一个 20 条的金标准评测集——每条是一句口语化的倾诉加上它应该命中的星球。然后写了个脚本跑批量评测直接输出 top-1 准确率。这让我在四小时里的每一次语料修改都有一个客观数字可以判断 “是变好了还是变坏了”而不是靠感觉。最终的准确率是 95%。图3 数据灌入与检测我没想到的一个关键结论写给向量看的文字和写给人看的文字不一样这是整个项目里我学到的最有价值的一件事。最初我写星球的“情绪特征”字段时很自然地写成了关键词堆叠机械 重复 忙碌 麻木 提不起劲 热闹却孤独 空转这看起来很“检索”对吧结果召回效果很差。用户说“再热闹也和谁都隔着一层提不起劲”系统经常命中不到 “点灯人”。我卡了一会儿然后意识到一件事我用的是密集向量模型而密集向量捕捉的是句子与句子之间的语义相似度——不是词与词的匹配。我给它一堆孤立的词等于把它最擅长的能力关掉了。于是我把每颗星球的签名改写成了一段自然语言 —— 写成“处在那种状态的人会怎么说话”日子像在原地打转每天重复同样的事忙到麻木、提不起劲就算身处热闹也觉得和周围隔着一层像个上了发条的人。准确率立刻上去了。因为现在用户那句话和这段签名是两个“人在说同一件事”的句子它们在向量空间里天然靠得很近。这个结论可以推广到任何做语义检索的人身上如果你的召回不好先别急着调参数、换模型、加 rerank。回头看看你的语料是怎么写的。把它从“关键词标签”改写成“真人会说出来的自然句”往往比任何参数调优都有效。换句话说——你的语料不是给倒排索引看的是给向量看的。而向量是被人类的句子训练出来的。关于agent instructions的几条经验我也低估了 instructions 的重要性。在 Agent Builder 里agent 的系统提示几乎就是产品逻辑本身。几条踩坑模型必须支持工具调用。这是最容易翻车的地方。DeepSeek-R1reasoner对 function calling 的支持很弱挂上去之后 agent 就只会聊天、不去查数据。但用 Elastic 内置的 Anthropic Claude Sonnet 4.5 丝滑多了而且不用另外调用简直太完美了。把索引和字段直接写进提示。因为我用的是内置的通用 search 和 ES|QL 工具agent 需要知道去哪儿找。我在 instructions 里明确列出了三个索引各有什么字段、什么时候该查哪个——这比造自定义工具省事得多。安全条款必须是最高优先级并且要显式压过角色扮演。这一条我测了很多遍。如果只是把安全要求写在提示的末尾agent 在长时间的“狐狸”角色扮演之后遇到危机信号有可能继续用比喻式的语气回应。必须明确写这条原则覆盖以上一切检测到严重痛苦时立刻停止所有比喻。自定义 instructions 应该像代码一样对待要精确、要测边界情况、要迭代。我为安全路径专门准备了两组对照测试输入普通低落 vs 严重痛苦信号每改一次提示就跑一遍。用A2A把六颗星球变成六个独立人格如果只有一个 agent 在换语气那它就只是提示词技巧。我想要的是架构级的解耦。Agent Builder 原生支持 A2AAgent-to-Agent每个 agent 都有自己的 agent card 端点和协议通信端点用 API Key 鉴权。于是我把六颗星球做成了六个独立的 agent每个有自己的人格提示——点灯人是疲惫温和的国王是威严但其实放不下的商人是历经计算后归于平静的。狐狸作为编排者把用户的心境路由给最懂 ta 的那颗星球。一个实现细节A2A 目前不支持流式capabilities.streaming 是 false所有交互都是同步的 message/send任务跑完才返回完整响应。所以编排器的 timeout 要给足我给了 60 秒。另外我在编排器里做了容错——如果某颗星球连不上自动回退成狐狸自己回应。演示的时候这种保底比什么都重要。图4 星球概念图情绪星图把向量空间画出来这是整个作品里最有画面感的部分也是评委记住它的地方。我把用户的每一条情绪日志用同一个 e5 模型嵌入然后做 PCA 降到二维归一化成坐标渲染成一片夜空 —— 每一颗星是一句心事相似的心情自然聚在一起、连成星座最新的一颗带着玫瑰色的呼吸光晕。关键在于这些星星的位置不是我编的是每句心事在向量空间里的真实投影。演示的时候我全屏这片夜空鼠标慢慢划过几颗星让那些心事一句句浮现出来然后说“重要的东西眼睛看不见 —— 所以我们把它画成了星星。”技术上它并不复杂一个本地桥接脚本从 emotion-journal 拉数据调 Elasticsearch 的 _inference API 拿嵌入同一个 e5 端点零外部服务做 PCA吐给前端一个 Canvas。但它把整个系统里最抽象的东西 —— 向量 —— 变成了任何人都能看懂、并且会被打动的东西。图5 情绪星图负责任的设计递出号码和惊动真人做情绪类产品这一块不是加分项是底线。狐狸的 instructions 里有一条最高优先级的安全原则如果用户流露出强烈的绝望或严重痛苦立刻停止所有比喻式的陪伴用平实、真诚、不评判的语气表达关心并温柔地告诉对方可以拨打全国统一心理援助热线 1235624 小时免费不追问细节、不做风险评估式盘问、不提供任何具体方式。在这之上我还用 Elastic Workflow 做了一层当危机信号被识别会写入一个 crisis-alerts 索引一个 scheduled 的 workflow 定时扫描这个索引通过 Webhook connector 把告警推送到值守人员的微信上并把记录标记为已通知避免重复告警。也就是说负责任不只是递出一个号码 —— 系统会在安抚用户的同时惊动一个真正的人。还有一条设计我认为是这个产品的灵魂反依赖。狐狸每一次陪伴的结尾都会温柔地把用户推回真实世界——邀请 ta 今晚给一个真实的人发条消息、说句真心话。这既是《小王子》的结局小王子最终回到了他的玫瑰身边也恰好是最负责任的产品设计。它的目标不是留住你。图6 狐狸危机自动报警图7 监护人微信警告结果四个小时结束时系统跑通了完整链路混合语义检索、六个 A2A 星球 agent、ES|QL 驯服指数、向量投影的情绪星图、以及从 12356 到微信告警的负责任设计。最终拿了二等奖。但真正让我印象最深的是台上那 15 秒——当我全屏那片星图慢慢划过一颗星屏幕上浮现出一句“在人群里笑回家却一句话都不想说”的时候台下安静了一下。那一刻我意识到我们做的不是一个检索 demo。下一步是什么短期内我想继续打磨检索质量把金标准集从 20 条扩到更大规模覆盖更口语化、更迂回的表达把 PCA 换成 UMAP看看星座的聚类结构会不会更清晰驯服指数目前还是一个启发式公式未来可以演进成基于情绪改善斜率的指标。更长期的方向我在认真考虑把它做成一款单机的治愈游戏——六种孤独变成一座岛上的六处风景玩家通过陪伴让岛慢慢愈合。这样它可以完全离线、不收集任何情绪数据也就不需要用户把最脆弱的话交给一朵云。这个转向听起来像是放弃了 AI但对我来说恰恰相反hackathon 让我用向量搞清楚了“六种孤独该怎么被听懂”这个理解本身才是可以带走的东西。总结如果你要做的东西需要理解人真正想说但没说出口的话那么 Elastic Agent Builder 能给你的不只是一个检索工具 —— semantic_text 和 RRF 让你能接住言外之意ES|QL 让你能从时序里读出关系的深度A2A 让你能把一个笼统的 agent 拆成一群各有性格的角色而自定义 instructions 就是你的产品逻辑本身。整套东西在四个小时里成型比我预期的快得多。但如果只让我留下一句话那会是那个关于语料的发现你的语料不是写给索引的是写给向量的。而向量是被人类的句子训练出来的——所以请用人话写它。这句话花了我半小时的调试才明白但它可能是我从这场 hackathon 里带走的、最有用的东西。翟献帅北京教育学院数学与科学教育学院 讲师【一名对情绪与技术交叉领域感兴趣的开发者目前在做一款关于孤独的治愈游戏】本文中所描述的任何功能或特性的发布与时间安排均由Elastic全权决定。任何当前尚不可用的功能或特性可能无法按时交付甚至可能不会发布。在本文中我们可能使用或提及了第三方生成式AI工具这些工具由各自的所有者拥有并运营。Elastic不控制这些第三方工具对其内容、运行或使用不承担任何责任或法律义务也不对因使用这些工具而可能产生的任何损失或损害负责。请在使用AI工具处理个人、敏感或机密信息时保持谨慎。你提交的任何数据可能会被用于AI训练或其他用途无法保证你提供的信息会被安全或保密地存储或处理。在使用任何生成式AI工具之前你应当了解其隐私政策与使用条款。Elastic、Elasticsearch以及相关标识均为Elasticsearch B.V.在美国及其他国家/地区的商标、标志或注册商标。所有其他公司与产品名称均为其各自所有者的商标、标志或注册商标。

相关新闻