1. 感知系统到底是什么:为什么它是智能体的大脑入口,而不是传感器
做了几年智能体项目,我越来越觉得,多数团队对"感知系统"的理解是模糊的。大家一上来就调模型、写工具、接API,结果第一版demo经常是:用户丢过来一段话,直接拼进Prompt,模型输出一段答案——看起来能跑,但只要对话稍微长一点、输入稍微复杂一点,系统就开始"失忆""答非所问",甚至把前面几轮的信息当成当前问题处理。
问题的根源往往不在模型,而在感知层的设计。
1.1 智能体与传统程序的决定性差异,藏在"感知"而不是"推理"里
传统软件是确定性的:输入结构清晰,走完流程就有输出。智能体不是这样,它面对的是开放、非结构化、随时可能中断的输入——用户会打错字,会突然改口,会一次性上传十张图片,会追问"我说过的那个事情你记得吗"。
这时候智能体必须做的事,不是"生成回答",而是先把原始输入转化为自己能理解、能决策、能追溯的结构化信息。这就是感知系统。它决定了智能体看到的世界长什么样。
你给模型的上下文如果是一堆未经整理的聊天记录、日志、工具返回块,那模型看到的不是世界,是一团噪音。我习惯把感知系统类比成人体的眼睛和内耳前庭——它不仅是"接收外界信息",还要负责把信息变成身体的平衡感。没有感知层的智能体,就算模型再强,也像一个蒙着眼做手术的医生。
1.2 感知链路的五个环节:采集、清洗、结构化、组装、复盘
我在自己项目里,给感知层拆了五个环节,缺一个都会出问题。
- 采集(Ingestion):从消息队列、Webhook、文件上传、定时任务、数据库变更里把原始数据拉进来。
- 清洗(Cleansing):去掉乱码、去重、过滤异常值,把那些来自不同渠道的字段统一格式。
- 结构化(Structuring):把文本变成带角色、时间戳、类型、事件ID的JSON,把图片转成可被模型读到的Base64或URL。
- 组装(Context Assembly):按当前任务决定哪些历史信息进上下文,哪些压缩,哪些丢弃。这一步直接决定模型能不能"想起来"。
- 复盘(Post-hoc Review):跑完一轮后,把"感知到了什么"和"真实发生了什么"做对照。这个环节很少有人做,但它是感知系统持续进化的唯一途径。
你可能会觉得这像数据工程。对,感知层本质上就是"面向大模型的数据工程",只不过它的下游不是数仓报表,而是模型推理。
1.3 感知质量直接决定智能体的上限
我在上一个项目里被一句话刺激到了。客户方负责人看了我们的Demo,问了一个特别扎心的问题:"你们的智能体,连用户在上一句已经说过的城市名称都不记得,我凭什么信它能完成销售跟单?"
那个瞬间我意识到,推理模型再强,也只是"想法好",而感知系统负责"看得清、记得住"。两者叠加才是可信赖的智能体。感知拉胯,后面所有的规划、工具调用、行动决策都会跟着错。这就好比开车,方向盘和油门没问题,但前挡风玻璃是花的,你怎么踩油门都到不了目的地。
所以这一章我聊的就是感知系统怎么搭、怎么用、怎么避坑。内容适用于实际做智能体开发的工程师、产品经理,也适合准备做Agent项目的技术负责人。
2. 感知输入的三条主线:消息、事件与同步调用,先分清再动手
如果我只能给感知系统提一条建议,那就是:代码写得丑没关系,但数据流一定要理清楚。我用一个真实项目的例子来说明。
那是一个销售跟单智能体,要处理客户在各个渠道的咨询、订单变更、物流异常,还要在需要时主动给客户发消息。刚开始我天真地把所有感知源都接成一种"消息"格式,结果没多久就翻车了——定时任务触发的库存预警,和客户正常提问混在一起,模型分不清谁该优先响应,甚至闹过"客户问A产品能不能开发票,模型拿库存预警的信息去回答"的笑话。
2.1 消息、事件、调用:三种感知源,三种处理语义
我最终把感知输入分成三类,用完全不同的数据结构和处理逻辑来对待。
| 感知源类型 | 典型来源 | 语义特征 | 处理方式 |
|---|---|---|---|
| 消息型 | 用户聊天、IM消息、邮件 | 有明确发送者与接收者,通常期望回应 | 进入对话上下文,参与模型当前推理 |
| 事件型 | 定时任务、Webhook回调、系统告警 | 无明确对话方,代表系统或外部世界的状态变化 | 进入状态管理模块,由智能体判断是否需要行动,不直接进入上下文 |
| 同步调用型 | 工具调用返回、数据库查询结果、API响应 | 由智能体主动发起,结果必须被消费 | 作为当前推理步骤的一部分,临时注入上下文,任务完成后归档 |
这个分类不是拍脑袋,它对应着智能体三种不同的"注意力"需求。消息型需要长期跟踪,因为用户可能在五分钟后说"我刚说的那个价格你能再确认下吗";事件型只关心当前窗口,比如库存预警过了三小时就没意义;同步调用型是即时性的,用完就丢,长期保留反而污染上下文。
2.2 事件驱动还是轮询?在智能体场景里我一般选"混合"
有人喜欢把一切感知都做成Webhook事件驱动,也有人习惯定时轮询数据库。我在实际项目中是混合用的。
举一个具体例子:物流状态查询。轮询每五分钟跑一次,大部分时候返回"运输中",纯属浪费;但如果订单物流状态真的变了,又希望智能体能第一时间感知到。我的做法是:Webhook负责"关键节点变更"的实时通知(比如已签收、派送失败),轮询只做兜底,两小时一次,检查那些Webhook漏掉的静默变更。这样既控制了成本,又保证了感知完整性。
如果你把轮询频率调得太高,感知层会变成性能瓶颈,智能体的响应速度反而会下降。感知层的职责是"在需要的时候提供需要的信息",不是"把所有信息都拿到手里"。
2.3 内部感知与外部感知的归并:让模型清楚"谁在说话"
除了用户的公开消息,智能体系统里还有一类容易被忽略的感知源——内部状态。比如"智能体自己刚刚执行了哪个工具""上一次行动的结果是什么""当前任务进行到第几步"。
如果这些信息不从感知层统一管理,模型会经常把"自己做过的事"误认为是"用户说的话"。这在多轮工具调用的场景里特别致命。我在数据上给每个感知条目加了一个source字段,取值有user、system_notice、tool_result、agent_internal四种。模型拿到Prompt时,能清楚看到每一条信息来自哪里,就不会再把工具返回的"库存不足"当成用户的自述。
这一步说起来简单,但它是整个感知层结构化设计里最容易被低估的一环。
3. 感知管线的核心实践:从原始数据到"模型能理解的状态"
有了分类,下一步就是把感知的过程工程化。我带的小团队里有个默认规矩:任何感知输入,在进入模型上下文之前,必须经过一个明确的转换管线。没有这个管线,直接拼原始文本的,一律打回重写。
3.1 输入归一化:先别管内容,把格式统一成"四条元数据"
每一条感知信息,进入系统后不管来自哪个渠道,我都会先归一化成四条元数据,再加上载荷(payload)。
{ "source": "user", "type": "text_message", "timestamp": 1739260800, "session_id": "cust_order_8823", "payload": { "text": "我想把订单里那件蓝色外套换成L码", "attachments": [] } }- source:来源,见上文分类
- type:消息子类型,比如text_message、order_update、image_input
- timestamp:事件发生的服务器UTC时间,不是接收时间
- session_id:会话归属,用于上下文按会话隔离
这四条元数据的重要性,我在后面讲上下文组装时会反复提到。特别是timestamp,很多智能体翻车都是因为忽略了时间属性,模型把昨天的事务当成当前状态,导致给出错误判断。
这里顺便给新手一个补充:所有时间戳一律用UTC时间戳存储,展示层再转本地时区。我见过太多团队存本地时间,多时区跑起来全是坑。
3.2 上下文组装:三层滑动窗口,比"把所有历史全塞进去"强十倍
上下文窗口一直是智能体感知层的硬约束。哪怕是128K的模型,也不能把所有历史都无脑塞进去——信息密度和干扰信号是并存的。
我在实际项目里使用的是三层上下文结构:
- 当前窗口(最近10轮对话或当前任务快照,约3000~4000字):包含本轮用户输入、上一个已完成的工具返回、当前活跃目标。模型做下一次推理时,主要依靠这一层。
- 近期摘要(最近30轮,约1500~2000字):对中间历史做摘要,保留关键事实、已确认信息、待办项。摘要不是逐条翻译,而是提炼"用户说了什么决定""系统完成了什么动作"。
- 长期存储(超过30轮或跨会话):进向量数据库,不直接注入上下文。只有在检测到当前话题与历史话题明显相关时才触发检索召回。
这三层不是固定比例,而是要时刻守住"当前窗口足够具体、近期摘要足够精炼、长期存储足够出有感召力"的原则。
一个我在踩坑后总结的经验:不要在Prompt里把所有工具返回都一五一十保留。举个例子,智能体调天气接口返回了几百字的JSON,但模型其实只需要"明天上海多云 22~28度"这几个字。感知层要做"语义压缩",把工具返回压成一句话再进上下文,模型推理速度会改善很多,准确率往往也会提升,因为噪音少了。
3.3 状态跟踪器:让智能体始终知道"自己干到哪了"
感知系统不仅仅是被动接收信息,它还要主动维护一个"世界状态"。我会在内存里放一个轻量的状态跟踪器,保存当前会话的关键状态:
{ "session_id": "cust_order_8823", "current_task": "处理换货申请", "step": 3, "collected_facts": { "customer_city": "上海", "order_id": "ORD202602130028", "exchange_size": "L" }, "pending_questions": ["是否需要补差价"], "last_verified_at": 1739261050 }这个状态跟踪器的存在价值是:模型是"无状态"的,每次调用都只有上下文;而智能体作为系统,必须有一个地方把关键事实沉淀下来。每完成一轮交互,我就用一轮新的推理结果去更新状态跟踪器,然后把更新后的状态摘要放进当前窗口。
状态跟踪器可以简单也可以复杂。复杂一点的,会加上一个"不确定性队列",记录智能体还没确认的信息,等下次感知到相关输入时优先确认。这个设计让我在做客服智能体时特别省心——模型不会因为用户没有直接回答某个问题就遗忘它,系统会一直追着这个没闭合的问题。
4. 多模态感知的工程落地:别让图片和音频成为系统的短板
如果说文本感知是地基,那多模态感知就是智能体的"视力"和"听力"。这两年做智能体,遇到多模态输入已经是常态了。客服智能体要接收用户发的商品照片、截图;销售智能体要读取合同PDF里的关键字段;运维智能体要看监控面板的截图判断异常。这部分的构建经验,值得专门拿出来聊。
4.1 先判断场景是否需要"真多模态"还是"伪多模态"
很多团队把"接收图片"和"多模态感知能力"画了等号,这是个误区。我用过一个项目里最开始的方案:用户发一张商品实拍图,系统拿到图片后先调OCR模型抽文字,再把文字塞进Prompt。这种方案在"识别商品名称、订单编号"这种场景下是够用的,成本低,响应快。
但换到"判断商品是否有瑕疵"这类场景就彻底不行了。文字描述不出来几张图片里产品外观的细微差别,必须把图片本身送给视觉模型。所以我现在会先做能力矩阵分析:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 识别票据编号/订单号 | OCR + 文本注入 | 快、便宜、准确率足够 |
| 识别商品外观/瑕疵/场景 | 图片直传视觉模型 | 需要像素级理解 |
| 识别谈话中的情绪/指令 | 语音识别 + 音频特征或许需要视觉模型 | 端到端或管线看场景 |
一句话总结:能用文本完成的感知,不要上多模态模型;必须上多模态的时候,要把它当成一条独立的感知链路来处理,而不是在文本链路里打个补丁。
4.2 多模态感知的时延控制,是工程上最容易被忽视的坑
你如果让视觉模型直接处理一张4K原图,一次推理可能要8~10秒,这个时延在客服对话场景里会直接造成"用户等待超时"的体验灾难。我的做法是:
- 感知层先统一把图片降到"模型可用但成本可控"的尺寸,比如最长边2560px。
- 对超长图片做切片,比如一张长截图分成多块,分别走视觉模型,再合并结果。
- 给多模态感知单独设定超时上限,超过5秒就降级为"告知用户图片正在分析中,稍后回复",不阻塞主流程。
我在一个项目里实测过,同样的视觉任务,把图片从4K缩到1080P,模型准确率只下降了不到2%,但推理时间从8秒降到2秒。这个性价比很多人没有意识到。多模态感知的关键不是追求信息无损,而是追求"在业务容忍的时延和成本内,拿到足够准确的感知结果"。
4.3 把多模态输出重新注入上下文:不是所有模型都原生支持
还有一个常见误区——你用的主模型可能不支持图片输入,但感知层拿到了图片信息。这时候需要做一个"模态桥接":用一个视觉模型把图片"翻译"成结构化文本描述,再把这描述作为感知结果给主模型。
# 伪代码示例:多模态感知桥接 def perception_pipeline(image_bytes): # 降质与尺寸统一 processed_image = image_preprocess(image_bytes, max_edge=2560) # 视觉模型提取结构化描述 vision_prompt = "请从图片中提取:商品类型、品牌、瑕疵描述、图片背景信息" perception_result = vision_model.generate(processed_image, vision_prompt) # 把视觉感知结果转为文本注入主模型上下文 return context_block(role="vision_perception", content=perception_result, timestamp=now())这套设计的经验是:感知层越早把高维信息翻译成语义信息,后续所有组件就越省事。视觉模型负责看懂,主模型负责想明白,各司其职。如果主模型本身支持图片,那可以直接把处理后图片交给主模型;否则就走桥接方案,反正不能让图片信息卡在感知层出不去。
5. 记忆感知:让智能体"虽然每次都是新模型,但看起来像记着你"
如果把感知系统只理解成"当前输入的获取",又错了。对智能体来说,历史本身就是一种感知对象。用户说"我之前问过你那个事",如果感知层不能把"之前"捞回来,这个对话就断了。
我在前文提过三层上下文窗口,这里展开讲记忆与感知的结合。
5.1 短期记忆与长期记忆的边界,取决于"会话"而非"时间"
有人按时间划分记忆,比如一天内算短期,一天外算长期。我的经验是按会话切。一次跟单任务可能持续三周,三周内的状态都应该视为短期记忆,每次推理都要带上关键事实;而某个客户去年提过的偏好,可以算长期记忆,不需要每次加载,但要在相关时刻被检索出来。
所以我在实现里,"短期记忆"就是当前session的状态跟踪器 + 近期摘要,"长期记忆"是向量数据库里跨session沉淀的用户画像、历史任务结论、偏好标签。
5.2 向量存储踩过的坑:切片粒度、相似度阈值与增量索引
第一个坑是切片粒度。切片太大,检索召回时容易命中一段无意义的长文本;切片太小,又丢了上下文。我最终把文本切片控制在200~400个汉字,重叠50~100字。像客服对话这种结构化数据,我建议按"单个完整语义单元"切,比如一次完整的客户提问+智能体回答,而不是机械按字符切。
第二个坑是相似度阈值。默认的0.7阈值在很多场景下根本不适用。我做了一轮标注实验,最终对我们的客服场景把阈值定在0.78左右。低于这个分数的召回,基本都是噪音。而且不同业务域的相似度分布不同,建议你每次接新项目时,都找几组正负样本重新标定一次,别偷懒沿用上个项目的阈值。
第三个坑是增量更新。用户在一通对话中改了三次收货地址,向量库里可能存了三个版本。我的做法是:长期记忆只存"最新确认"状态,并且每次更新时先按session_id把旧条目标记失效。不这样处理,智能体就会把用户已经改掉的旧地址误以为是当前地址,后果在真实业务里很严重。
5.3 主动召回与被动注入:记忆也要讲时机
记忆不是越多越好。我把记忆感知分为两种时机:
- 被动注入:在系统启动或每轮对话开始时,自动拉取当前session的短期记忆,放进入口上下文。
- 主动召回:当模型在当前轮推理里发现信息不足时,由系统触发一次检索调用,把相关长期记忆临时注入下一轮上下文。
主动召回的前提是,感知层能够输出"信息不足"的信号。我在状态跟踪器里加了一个confident_score字段,模型对某个关键事实没有把握时,就把分数调低,触发记忆检索。这个机制让我的智能体不再"一本正经胡说八道"——它知道自己不确定,然后去"翻记忆"而不是硬答。
6. 感知层必须提前做对的四件"脏活":容错、时序、同步与隐私
讲到这里,感知系统的主干已经清楚了。但真正决定系统在生产环境能不能活的,往往是四件"不起眼"的细节。
6.1 感知输入的超时与重放:不能让外部服务把智能体拖死
感知不只是从用户那里拿输入,很多时候是从外部API、数据库、文件系统里拿数据。这些外部依赖都可能超时或失败。我定的规则是:
- 所有感知调用必须有超时上限,默认3秒,最长不超过5秒。
- 失败后区分"可重试"和"不可重试"。网络超时、限流返回可重试,业务校验失败不可重试。
- 重试必须带backoff策略(比如1秒、2秒、4秒),最多重试三次。
- 重试期间不能让主流程一直等待。先把"感知失败"作为上下文里的一个fact告诉模型,让它决定是继续等、换个路径、还是向用户解释。
这套容错逻辑,让我的智能体在外部服务不稳定时依然保持可用。感知层的核心KPI不是"成功率100%",而是"失败时可优雅降级"。
6.2 时间戳对齐:LLM没有与生俱来的时序感,时序靠感知层给
大模型内部的注意力机制并不天然理解"先后顺序"。如果你的感知层不强制给每条信息带上时间戳,模型很可能把今天的库存数据和昨天的订单请求混在一起推理。
我在感知层做了一件所有人看起来有点笨的事:在给模型的上下文文本里,每条感知信息前面都会加上一个自然语言的时间前缀,比如"(12秒前)(上午10:23来自用户)"。这让模型在推理时能直接感知到谁先谁后。
另外,状态跟踪器里维护的last_verified_at也很关键。如果某条关键事实已经很久没被验证过,系统会在推理前主动触发一次"确认型感知",去数据库或外部系统核对一下,避免用过期信息做决策。
6.3 感知与行动的数据一致性:读写不同库,迟早出问题
感知层必须把数据落库,行动层(工具执行)也会更新数据。这两部分如果跨库操作,一致性问题迟早爆雷。我的方案很简单:感知状态全部走Redis缓存加速读,关键业务状态以MySQL为准,写入走事务,读优先走缓存但必须在修改时双写并设置过期。
说人话:感知层快速读缓存,确认信息够新就继续;不够新或修改后,一定回到主库。这个"读写路径分离"设计在智能体这种高频读、低频写、但对实时性有要求的场景里,算是性价比最高的方案。
6.4 别把敏感信息全塞进感知上下文
感知层会接触到身份证号、手机号、地址、甚至聊天记录里的隐私信息。我见过有团队把这些明文数据一股脑全放Prompt里,一个人工审计就炸了。
我的方案是分级脱敏:感知层内置一个脱敏引擎,凡是匹配手机号、银行卡、身份证的片段,默认用"***"替换。只有在某个工具确实需要这些字段时,才按权限临时解密一次,用完即焚。另外,长期记忆存储前,统一做隐私字段的归一化和脱敏,向量库里只存打标后的描述。
做智能体感知系统,先想好哪些数据能进模型上下文、哪些绝不能进,这个边界定得越早,后面越省事。
7. 我记忆深刻的几个感知层翻车案例,以及它们如何改变我的设计原则
理论知识再多,没有一次翻车经历来得深刻。这里记录三个让我印象深刻的感知层事故,每个背后都对应一条我现在严守的设计原则。
7.1 翻车一:图片被感知层"降质"得太过分,视觉识别直接崩溃
项目背景是让智能体识别用户上传的订单截图,提取订单号和金额。一开始为了省成本,我把图片压到最低分辨率,结果视觉模型连续三天把"8721"识别成"872l",把金额的千分位看丢。
那次我意识到,多模态感知在做压缩时,不能只看体积和时延,还要关注关键信息的保真度。后来我调整为两级策略:初次识别用低分辨率图,敏感字段识别用高分辨率图,双重校验。准确率从86%提升到了98.5%。代价是多了一次视觉调用,但换来的是客户不投诉。
7.2 翻车二:上下文里垃圾信息太多,模型跟用户吵起来了
这是我最丢人的一次事故。我们的销售智能体在一次测试里,莫名其妙对用户说出了"您刚才的方案已经被否了,请不要再问"这种话。查了很久才发现,原因是有条历史工具返回记录里包含"方案被上级驳回"的负面情绪词,这条记录本来应该属于另一个session的,因为感知层会话隔离没做好,混到了当前上下文。
从那以后,我写了两个硬性规矩:一是session_id必须作为感知条目的强制字段,二是上下文组装器在拼接前先做一次session过滤。你永远要假设模型会把上下文中看到的任何一句话都当成事实,所以感知层必须在根源上保证只给模型看该看的东西。
7.3 翻车三:感知盲区——系统不知道"自己不知道"
有段时间智能体在处理订单问题时表现很怪:客户问"在吗",它直接回复了一大段"您好,我这边查不到订单信息,请提供订单号"。
事后分析,智能体并没有"觉得"信息缺失,它只是在没有上下文的情况下盲目生成了回复。这就是典型的感知盲区——系统没有机制感知到"当前信息不足以回答"。
后来我引入了前文提到的confident_score机制。每当模型输出的置信度低于某个阈值,状态跟踪器就标记一条pending_question,下一轮感知优先去补齐,而不是让模型硬答。这个简单的改动,把"答非所问"率从8.3%降到了1.7%。
8. 一点没有写在教科书里的体会
回看这些经验,感知系统构建的核心其实不是某个具体技术,而是一套"把世界变成模型能理解的上下文"的思维方式。
模型每一次生成都只有一次机会;感知系统负责让这次机会不被浪费。
如果你正准备搭建智能体,我建议不要先把Prompt调得天花乱坠,而是先把感知层的数据模型设计好:理清三类感知源的语义,设计好元数据字段,规划好上下文组装策略,再考虑多模态和记忆。这样就算后面换模型、换框架,感知层依然可以稳定复用。
最后分享一个我常用的验证方法:每个月抽查二十条真实对话,比对"模型回复依据的上下文"和"真实发生的事实",凡是有偏差的,追到感知层去找原因。坚持三个月,你的感知系统会比绝大多数公开Demo里的实现更扎实。
智能体做得越久,越会理解那句话:看得清,才想得对。感知不是开始,而是整个智能体项目的地基。