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

作者:翟献帅 | Elasticsearch Agent Builder Hackathon · 北京

本内容遵循 CC 4.0 BY-SA 版权协议

深夜的聊天框里,有人打下这样一句话:

"最近觉得,再热闹也和谁都隔着一层,提不起劲。"

这句话里,没有"孤独"两个字。没有"抑郁",没有"难过",没有任何一个情绪词典里的词。

任何基于关键词的系统,都会漏掉它。

而这恰恰是问题所在:人在表达孤独的时候,几乎从不使用"孤独"这个词。他们说的是 "隔着一层",是 "提不起劲",是 "忙到麻木",是"拥有的不少,却不知道为了什么"。情绪的表达天然是隐喻的、迂回的、绕着走的------这也是为什么,在"情绪"这件事上,关键词匹配从一开始就注定失败。

7 月 11 日,我参加了 Elastic 与阿里云在北京举办的 Agent Builder Hacknight。四个小时,从零交付一个可演示的原型。我没有花时间去找题目 ------ 我想做的东西已经在我脑子里放了很久:一个能真正 "听懂" 这种话的陪伴 agent。

最后它拿了二等奖。但比名次更有价值的,是这四个小时里我踩过的坑、以及一个我完全没有预料到的结论。

为什么 "听懂孤独" 很难

我们身处一个史上最 "连接" 的时代------消息秒回、社交不断。可越来越多的人,在最热闹的房间里,依然觉得没有人真的懂自己。

现有的方案大致分两类。一类是专业心理服务,它回答的是 "你有没有病";另一类是各种情绪打卡、心情记录 App,它给你一个标签:今天你是 "开心" 还是 "难过"。

但很多人真正想问的,其实是第三个问题:"有没有人,愿意先听我说。"

而从技术角度看,难点非常具体:这类表达的语义几乎完全藏在字面之下。"再热闹也隔着一层"和"机械重复里的倦怠"之间,没有任何一个共同的关键词,但它们说的是同一种感受。要接住这句话,系统必须理解的是句子的"言外之意",而不是它的词。

这正好是向量语义检索最擅长、也最该被用在的地方。

系统:一只狐狸,和六颗 "孤独星球"

我在 Elastic Cloud Serverless(Elasticsearch 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 字段做 BM25:

"signature_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-R1(reasoner)对 function calling 的支持很弱,挂上去之后 agent 就只会聊天、不去查数据。但用 Elastic 内置的 Anthropic Claude Sonnet 4.5 丝滑多了,而且不用另外调用,简直太完美了。
  • **把索引和字段直接写进提示。**因为我用的是内置的通用 search 和 ES|QL 工具,agent 需要知道去哪儿找。我在 instructions 里明确列出了三个索引各有什么字段、什么时候该查哪个------这比造自定义工具省事得多。
  • **安全条款必须是最高优先级,并且要显式压过角色扮演。**这一条我测了很多遍。如果只是把安全要求写在提示的末尾,agent 在长时间的"狐狸"角色扮演之后,遇到危机信号有可能继续用比喻式的语气回应。必须明确写:这条原则覆盖以上一切,检测到严重痛苦时立刻停止所有比喻。

自定义 instructions 应该像代码一样对待:要精确、要测边界情况、要迭代。我为安全路径专门准备了两组对照测试输入(普通低落 vs 严重痛苦信号),每改一次提示就跑一遍。

A2A 把六颗星球变成六个独立人格

如果只有一个 agent 在换语气,那它就只是提示词技巧。我想要的是架构级的解耦。

Agent Builder 原生支持 A2A(Agent-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 里有一条最高优先级的安全原则:如果用户流露出强烈的绝望或严重痛苦,立刻停止所有比喻式的陪伴,用平实、真诚、不评判的语气表达关心,并温柔地告诉对方可以拨打全国统一心理援助热线 12356(24 小时免费),不追问细节、不做风险评估式盘问、不提供任何具体方式。

在这之上,我还用 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. 在美国及其他国家/地区的商标、标志或注册商标。所有其他公司与产品名称均为其各自所有者的商标、标志或注册商标。

相关推荐
Elasticsearch2 小时前
你的 AI 表现不佳。你的数据基础才是问题
elasticsearch
leoZ2312 小时前
Git 集成实战完全指南(三):自动化 Commit 与 PR
大数据·git·elasticsearch
Elasticsearch3 小时前
Elastic: 将来一年中 AI 从活动转向成果
elasticsearch
Chockmans5 小时前
春秋云境CVE-2023-51385(保姆级教学)
大数据·web安全·elasticsearch·搜索引擎·网络安全·春秋云境·cve-2023-51385
Elasticsearch5 小时前
一张图片胜过 1.5 倍的文字:我们从产品搜索 embeddings 基准测试中学到了什么
elasticsearch
吉甫作诵6 小时前
Elasticsearch 常用运维命令大全
运维·elasticsearch
干了这杯柠檬多15 小时前
Elasticsearch 深度分页:Scroll vs PIT + search_after
elasticsearch
leoZ23116 小时前
Git 集成实战完全指南(二):智能分支管理
大数据·git·elasticsearch
Elastic 中国社区官方博客1 天前
4 个英伟达人工智能任务,1 个 Elasticsearch 接口:嵌入、聊天、completion 和重排序
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai