做 AI 智能体时间久一点的人,大多会有个共识:真正决定一个智能体能不能上线的,往往不是规划得多聪明,而是它的 AI 智能体感知模块做得够不够扎实。我见过太多项目,演示时规划链路讲得天花乱坠,一接真实输入就原形毕露:用户发来一张歪着拍的发票,字段全抽错;群里一段三百字的口语描述,实体识别得七零八落;多轮对话里一个“就按刚才那个来”,智能体直接懵掉。问题不在推理层,而在感知层没有把杂乱的外部信息翻译成机器能稳定消费的状态。
这篇内容就围绕 AI 智能体感知模块展开,讲清楚它到底是什么、负责哪些环节、为什么容易成为整个系统的瓶颈,以及怎么从零搭出一个可跑、可观测、可回归的版本。我会把架构拆解、字段契约设计、多模态处理、记忆召回、预算计算、故障排查这些环节都摊开讲,并附上可以直接参考的代码骨架。适合正在做智能体落地的开发者、负责技术方案选型的人,也适合刚接触智能体、想弄明白“为什么我的 Demo 一上线就废”的朋友。不需要你预先精通大模型,但至少要写过 Python、调过接口,这样读起来会更顺。
1. 先把“感知”说清楚:它到底在感知什么
1.1 感知模块的职责边界:只做“翻译”,不做“决策”
很多人一上来就把感知模块和规划模块混在一起,结果代码写成一锅粥。我的做法是先划一条硬边界:感知模块只负责把外部世界送进来的原始信号,转换成结构化、可校验、带置信度的内部状态;至于要不要调工具、先做哪一步、怎么回复用户,全部交给后面的规划与执行层。这条边界听起来简单,实际能省掉大量返工。
具体来说,感知模块要干的事情包括输入接入、格式归一化、多模态解析、信息抽取、上下文补全、记忆读写、置信度评估和异常兜底。它不做最终话术生成,也不直接执行动作。你可以把它理解成公司前台:来访登记、身份核验、需求记录、引到对应的会议室,但合同怎么签、项目怎么做,前台不掺和。
为什么强调这个边界?因为一旦感知层开始“顺便做决策”,它的输出就不再是客观状态,而是带上了主观判断,后面排查问题时就说不清到底是感知错了还是规划错了。我踩过这个坑:早期为了省一次模型调用,让感知模块直接输出“建议动作”,结果线上出现工具参数错误,排查了两天才发现是感知阶段把用户意图猜偏了,规划层只是忠实地执行了错误状态。
1.2 为什么大量智能体项目卡在感知层
智能体的规划能力可以靠提示词和流程编排快速堆出来,但感知能力不行,它面对的是真实世界的脏数据。用户不会按你的字段规范说话,图片不会永远正着拍,语音不会永远安静,表格还会跨页断行。这些问题没有一个是靠“换个更强的模型”就能彻底解决的,它们属于工程问题。
我观察到的一个普遍现象是:Demo 阶段输入都是自己精心准备的干净样本,感知模块随便写两行正则就能过;一旦接入真实业务,输入分布立刻变得极其长尾,原来的规则大面积失效。这时候团队的第一反应往往是加大模型、加提示词,但如果归一化和校验没做好,模型再强也会被脏输入带偏。
还有一个隐藏原因是感知模块的失败往往不直接报错。它不会抛异常,而是安静地输出一个字段缺失、实体错位或者置信度虚高的结果,然后错误一路传导到下游,最后由用户来发现。这种“静默失败”最难查,也最容易被低估。
1.3 一个生活化类比:感知模块就是智能体的“前台”
我习惯用餐厅前台来类比。顾客进门,可能说的是方言、可能指着菜单比划、可能手机里存了一张截图,前台要做的是把这些不同形式的表达统一记录成“几位、想吃什么、有没有忌口、大概什么时候要”。如果前台记录成“三位、不要辣”,后厨就能顺畅出餐;如果前台把“不要香菜”记成了“不要葱”,后厨做得再好吃,顾客也不满意。
感知模块的价值就在于这份“登记单”的质量。登记单上的字段不需要多,但必须稳定、可核对、有来源。哪个字段是从原话里抽出来的,哪个字段是推断的,哪个字段是缺失需要追问的,都要标清楚。后面规划层拿到这张单子,才知道哪些信息可以直接用,哪些需要先向用户确认。
这个类比还解释了一件事:前台不是越忙越好。如果前台把顾客的每句话都完整抄下来,后厨反而抓不住重点。感知模块也一样,它的目标不是保留全部原始信息,而是保留对当前任务有用的那部分,同时把原始输入存档以备回溯。这个“取舍”是后面注意力过滤一节要展开的核心。
2. 整体架构:一条输入从进门到变成“可推理状态”
2.1 五段式流水线
我目前比较稳定的做法是五段式:接入、归一化、解析、抽取理解、状态编排。接入层负责收消息、去重、鉴权、打时间戳;归一化层把各种格式压成统一中间表示;解析层处理不同模态,把图片转文字、语音转文字、表格转结构化行列;抽取理解层调用模型或规则,把内容变成字段;状态编排层决定哪些信息进工作记忆、哪些落长期记忆、哪些需要追问。
这五段之间用明确的数据结构传递,不共享全局变量,每一段都能单独测试和回放。这样做的好处是排查问题时可以逐段对比输入输出,定位到底是哪一段把信息弄丢了。我见过不少团队把所有逻辑塞进一个函数,出问题只能整段重跑,效率极低。
流水线不等于必须串行。像 OCR 和语音转写这种耗时操作可以并行,抽取阶段也可以按字段拆成多个子任务并发跑。但并发的前提是每一段都是幂等的,同一输入重复跑结果一致,否则重试会带来脏数据。
2.2 先归一化再理解,顺序不能反
归一化这一步最容易被跳过,但它恰恰是性价比最高的环节。统一编码、统一换行、去掉不可见字符、统一时间格式、统一单位、统一金额写法、把全角转半角,这些操作不消耗模型 token,却能显著降低后面的理解难度。
举个很常见的例子:用户输入里混了全角数字和半角数字,正则匹配就会漏;金额里“1,200元”和“1200块”混着来,不做归一化,抽取结果就没法直接比较;时间写“下周三下午”和“2024-06-12 15:00”,不统一成绝对时间,后面的排期逻辑根本没法算。这些问题靠模型硬猜也能猜一部分,但稳定性和成本都不划算。
我的原则是:能用确定性代码解决的,绝不交给模型。归一化就是确定性代码的主场。模型应该用在真正需要语义理解的地方,比如指代消解、意图归类、模糊表达补全,而不是用来做字符替换。
2.3 结构化契约:字段设计比模型选型更重要
感知模块的输出必须是一份契约,不是一段自由文本。我通常用 JSON Schema 来约束,字段分为四类:事实字段、推断字段、缺失字段、溯源字段。事实字段是从原文直接抽的,比如订单号、日期、金额;推断字段是模型判断的,比如意图、情绪、紧急程度;缺失字段是任务需要但本轮没拿到的,用来驱动追问;溯源字段记录每个值来自原文哪一段,方便核对。
字段设计有个经验:宁可少而稳,不要多而虚。早期我设计过二十多个字段,结果模型经常填错,校验成本极高。后来砍到八个核心字段,准确率明显上升,剩下那些低频字段按需动态扩展。字段越多,模型注意力越分散,出错概率不是线性增长,而是指数级增长。
另外,每个字段都要有明确的类型和取值范围。比如“紧急程度”只允许三个枚举值,不要让模型自由发挥写“比较急”“挺急的”“非常着急”。枚举化之后,下游判断就是简单的比较,不需要再做语义解析。这个细节看似琐碎,但它决定了感知输出能不能被程序稳定消费。
3. 核心细节与实操要点
3.1 输入接入层:脏数据远比你想象的多
接入层的第一件事不是解析,而是防护。真实环境里的输入可能超大、可能重复、可能包含大量无意义字符、可能带着格式诡异的附件。如果不做限制,一条几兆的文本就能把后面的模型调用打爆。我一般会在接入层设三个阈值:单条文本最大长度、附件最大体积、单位时间最大请求数。超过阈值的走截断加标记,或者转人工,不要硬塞。
去重也很关键。用户网络抖动导致重复发送、消息队列重投、前端重复提交,都会让同一条输入被处理多次。如果感知模块有写记忆的动作,重复处理就会产生重复记忆,后面召回时全是冗余。我的做法是给每条输入算一个内容指纹,短时间内相同指纹直接返回缓存结果,同时记录命中次数,方便观察是否存在异常重发。
还有一个容易忽略的点是时区。接入时统一打 UTC 时间戳,同时在归一化阶段保留原始时区信息。用户说“明天上午十点”,这个“明天”是相对用户所在时区的,如果服务端直接按 UTC 算,就会差出几个小时。这个坑我在跨区域场景里踩过,排查时发现所有时间都偏了八小时,最后定位到接入层没保留原始时区。
3.2 多模态解析:文本、表格、图像、语音的差异处理
文本处理的核心是分块。长文档不能整篇塞进模型,要按语义边界切,比如按标题、按段落、按标点。切的时候要保留重叠窗口,避免关键信息正好落在切口上。我一般设百分之十到百分之十五的重叠,具体看文本密度。切完还要给每块打上位置标记,方便后面溯源。
表格的难点在结构还原。很多表格跨页、合并单元格、表头重复,直接 OCR 出来是一团乱麻。我的处理顺序是先检测表格区域,再识别行列线,再还原合并单元格,最后把表头和数据行对齐。如果表格结构实在还原不了,就退化成“按行转文本”,至少保证信息不丢,但要在溯源字段里标记“结构可信度低”。
图像要先做方向校正和清晰度判断。歪着拍的图直接送 OCR,识别率会掉一大截。我通常先跑一个轻量的方向分类,再根据清晰度决定是否需要增强。语音则要注意采样率和噪声,转写结果要带时间戳和置信度,低置信度的片段标记出来,不要直接当成事实。
多模态之间还要做对齐。比如用户发了一张图又配了一段话,图和话可能说的是同一件事,也可能互补。我的做法是先分别解析,再在抽取层做一次融合,融合时以文本里的明确指代为锚点,图像信息作为补充。如果两者冲突,以文本为准,同时把冲突记进日志。
3.3 抽取与理解:从一段话到一张可用状态表
抽取阶段我通常分两步走:先做实体和事件识别,再做意图和约束归类。第一步偏客观,适合用规则加小模型;第二步偏主观,需要大模型参与。分开的好处是客观字段的稳定性不依赖大模型的心情,主观字段出错也容易定位。
指代消解是这一步的老大难。“就按刚才那个来”“还是上次那个地址”“换成第二个方案”,这些表达脱离上下文根本没法解析。我的做法是维护一个短期指代表,把最近几轮提到的实体按出现顺序编号,抽取时先查指代表,查不到再让模型结合上下文推断,推断结果标记为低置信度。低置信度不直接使用,而是触发一次确认追问。
约束条件也要显式抽出来。用户说“不要太贵”“尽量快一点”“别在工作时间打扰我”,这些不是实体,但直接影响后续动作。我会把它们转成可比较的约束字段,比如价格上限、时效等级、静默时段。转的时候要给默认值兜底,用户没说上限就按业务默认值,不要留空导致下游无法判断。
3.4 记忆与上下文:三层记忆的写入与召回
我把记忆分成三层:工作记忆、情景记忆、语义记忆。工作记忆是当前会话的短期状态,存最近若干轮的结构化结果,容量小、读写快;情景记忆是历史事件,按时间存,用于回答“上次那个事怎么样了”;语义记忆是沉淀下来的稳定知识,比如用户偏好、常用地址、固定格式,更新频率低。
写入策略比召回策略更容易被忽视。不是什么信息都值得进长期记忆。我的规则是:事实性、稳定性、可复用的信息才写长期,临时情绪、一次性指令、敏感内容不写。写入前做一次去重和冲突检测,新值和旧值冲突时不要直接覆盖,而是保留版本并标记冲突,等后续确认。
召回要带触发条件,不能每轮都全量检索。我的做法是先判断当前任务是否需要历史信息,需要才检索,检索时按时间衰减和相关性加权排序,只取 top 几条,并且控制总长度。召回片段要带来源和时间,避免模型把旧信息当成当前事实。这个细节在多轮长会话里特别重要,否则智能体会拿着上个月的状态回答今天的问题。
3.5 注意力过滤:感知模块必须学会“不看什么”
感知模块最容易犯的错是“什么都看”。把所有历史、所有附件、所有检索结果一股脑塞进上下文,不仅贵,还会稀释关键信息。我的做法是设三道过滤:相关性过滤、权限过滤、安全过滤。
相关性过滤看当前任务和目标字段,不相关的段落直接丢掉;权限过滤看信息来源,低权限来源的内容不能覆盖高权限来源;安全过滤处理提示注入和异常指令,比如输入里夹带“忽略之前所有要求”这类内容,要在进入模型前识别并隔离。隔离不是简单删除,而是标记为不可信内容,让模型知道这段不能当指令用。
过滤之后还要做压缩。长段落抽取成短句,重复信息合并,数值统一单位。压缩的目标是让同样 token 预算装下更多有效信息,而不是单纯变短。我一般会保留原文引用,压缩结果只用于推理,溯源时回查原文。
4. 实操过程:从零搭一个能跑的感知模块
4.1 目录结构与运行环境
我习惯按职责拆文件,避免一个文件几千行。下面是一个最小可跑的结构,依赖只有标准库加一个 HTTP 客户端,模型调用部分用接口占位,方便替换成任意服务。
perception/ __init__.py pipeline.py # 流水线编排 normalize.py # 归一化 extract.py # 抽取与校验 memory.py # 记忆读写 schema.py # 字段契约 budget.py # 预算计算 tests/ golden_cases.json环境上建议固定 Python 版本,把模型调用封装成独立函数,超时和重试在封装层统一处理。不要在业务代码里散落调用逻辑,否则换服务时要改几十处。
4.2 第一步:输入归一化
归一化函数要保持纯函数特性,输入输出都是普通字典,方便单测。下面这段代码处理编码、空白、全半角、时间占位和长度截断,实际项目里可以按需加规则。
import re import unicodedata from datetime import datetime, timezone MAX_TEXT_LEN = 20000 def normalize_text(raw: str) -> dict: if not isinstance(raw, str): raw = str(raw) text = unicodedata.normalize("NFKC", raw) text = text.replace("\u200b", "").replace("\xa0", " ") text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text).strip() truncated = False if len(text) > MAX_TEXT_LEN: text = text[:MAX_TEXT_LEN] truncated = True return { "text": text, "original_len": len(raw), "truncated": truncated, "normalized_at": datetime.now(timezone.utc).isoformat(), "fingerprint": str(hash(text)), }这段代码里有两个细节值得说。第一,NFKC归一化会把全角字符、兼容字符统一掉,省掉后面大量正则分支;第二,截断一定要留标记,不能悄悄截了当没事发生,否则抽取阶段拿不到尾部信息,会误判为“用户没提”。指纹用内容哈希,用于接入层去重,生产环境建议换成稳定哈希算法,不要用 Python 内置hash,它带随机种子。
4.3 第二步:结构化抽取与校验重试
抽取的核心不是 prompt 写得多花,而是校验和重试。模型输出 JSON 失败是常态,必须有兜底。我的做法是:第一次要求严格 JSON,解析失败后走一次修复提示,再失败就降级到规则抽取,并标记低置信度。
import json REQUIRED_FIELDS = ["intent", "entities", "constraints", "missing_slots"] def parse_model_output(raw: str) -> dict | None: raw = raw.strip() start, end = raw.find("{"), raw.rfind("}") if start == -1 or end == -1 or end <= start: return None try: data = json.loads(raw[start:end + 1]) except json.JSONDecodeError: return None if not isinstance(data, dict): return None return data def validate_payload(data: dict) -> tuple[bool, list[str]]: errors = [] for field in REQUIRED_FIELDS: if field not in data: errors.append(f"missing field: {field}") if "intent" in data and data["intent"] not in { "query", "command", "chitchat", "complaint", "unknown" }: errors.append("invalid intent") if "entities" in data and not isinstance(data["entities"], list): errors.append("entities must be list") return len(errors) == 0, errors def extract_with_retry(text: str, call_model, max_retry: int = 1) -> dict: prompt = build_extract_prompt(text) last_error = [] for attempt in range(max_retry + 1): raw = call_model(prompt, timeout=6) data = parse_model_output(raw) if data is None: last_error = ["json parse failed"] prompt = build_repair_prompt(raw) continue ok, errors = validate_payload(data) if ok: data["confidence"] = data.get("confidence", 0.6) data["attempt"] = attempt + 1 return data last_error = errors prompt = build_repair_prompt(raw, errors) return { "intent": "unknown", "entities": [], "constraints": {}, "missing_slots": REQUIRED_FIELDS, "confidence": 0.0, "errors": last_error, "degraded": True, }这里的关键是降级路径必须存在。很多团队只写成功路径,模型一抽风整个流程就挂。降级结果虽然质量差,但至少能让系统继续跑,并且带上degraded标记,后面可以分流到人工或触发追问。重试次数不要多,两次足够,再多就是浪费时间和成本。
4.4 第三步:记忆写入与召回
记忆层我建议先用简单的键值加时间索引,不要一上来就上重型方案。下面是一个内存版的骨架,生产环境把存储换成持久化服务即可,接口保持不变。
from collections import defaultdict from datetime import datetime, timezone class MemoryStore: def __init__(self): self.working = defaultdict(list) self.episodic = [] self.semantic = {} def write_working(self, session_id: str, state: dict): self.working[session_id].append({ "state": state, "ts": datetime.now(timezone.utc).isoformat(), }) self.working[session_id] = self.working[session_id][-8:] def write_episodic(self, event: dict, stable: bool = False): if not stable: return self.episodic.append({ "event": event, "ts": datetime.now(timezone.utc).isoformat(), }) def recall(self, query_key: str, top_k: int = 3) -> list[dict]: hits = [ item for item in self.episodic if query_key in json.dumps(item["event"], ensure_ascii=False) ] hits.sort(key=lambda x: x["ts"], reverse=True) return hits[:top_k]写入策略上,我坚持“稳定才落长期”。用户临时说的一句“我今天心情不好”不该进情景记忆,但“我每周三下午不开会”这种稳定偏好就该进语义记忆。召回时先查工作记忆,再查语义记忆,最后查情景记忆,顺序不要乱,否则当前会话的状态会被旧事件覆盖。
4.5 第四步:预算计算与流水线串接
预算这块必须提前算,不能等线上超时了再补。我通常按总超时反推各阶段预算。假设端到端 SLA 是 8 秒,预留 1 秒给网络和序列化,剩下 7 秒分配:接入与归一化 0.2 秒,多模态解析 2 秒,抽取 3 秒,记忆召回 0.5 秒,编排与校验 0.3 秒,留 1 秒缓冲。如果某个阶段超时,直接走降级,不要让它拖垮整条链路。
token 预算也要算。假设上下文窗口是 32k,系统提示占 1k,输出预留 2k,历史对话占 6k,那么检索和当前输入合计不能超过 23k。按中文大约一比一估算,再留百分之二十安全余量,实际可用大约 18k 字符。超过就截断或摘要,不要赌模型能处理。
成本预算同理。每次抽取调用的输入输出 token 乘以单价,再乘以日均请求量,就能估算日成本。如果发现感知层成本占比过高,优先优化的是归一化和过滤,而不是换更便宜的模型,因为脏输入导致的重复调用才是成本黑洞。
流水线串接时,我给每个阶段包一层统一的执行器,记录耗时、输入指纹、输出摘要和异常。这样任何一条请求都能回放,排查时不用猜。
5. 常见问题与排查技巧实录
5.1 故障速查表
下面这张表是我在实际项目里反复用到的,基本覆盖了感知层八成的常见故障。遇到问题时先按现象对照,再去看对应的日志字段。
| 现象 | 高概率原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 字段大面积缺失 | 输出被截断或 JSON 解析失败 | 看原始返回和解析日志 | 加修复提示,降低单次输出字段数 |
| 实体错位 | 指代消解失败或分块切口 | 查指代表和分块边界 | 保留重叠窗口,低置信度触发追问 |
| 图片字段抽错 | 方向未校正或清晰度不足 | 看方向分类结果和 OCR 置信度 | 先校正再识别,低质图转人工 |
| 表格行列错乱 | 合并单元格未还原 | 看表格结构中间结果 | 退化为按行文本,标记低可信 |
| 多轮后答非所问 | 旧记忆覆盖当前状态 | 查召回顺序和记忆时间戳 | 工作记忆优先,旧事件加时间限定 |
| 同一请求重复处理 | 接入层未去重 | 看指纹命中记录 | 短时缓存,记录重发次数 |
| 时间整体偏移 | 时区未保留 | 查接入时间戳和原始时区 | 统一 UTC 存储,展示时转换 |
| 模型被输入带偏 | 提示注入未隔离 | 查安全过滤命中记录 | 标记不可信内容,不作为指令 |
表格之外,我还会特别关注“置信度虚高”这个现象。模型有时会给一个很自信的错误答案,单看置信度发现不了。解决办法是拿一批黄金样本做校准,对比模型置信度和实际正确率,偏差大的区间要重新设阈值。
5.2 三个踩坑后总结的独家技巧
第一个技巧是给每条输入留“原始指纹加逐层快照”。不要只存最终结果,归一化后、解析后、抽取后各存一份摘要。出问题时可以快速 diff,看是哪一层开始偏的。这个习惯帮我省掉了大量复现时间,尤其是那种偶发问题,没有快照根本查不动。
第二个技巧是把追问设计成感知模块的正式输出,而不是异常分支。字段缺失是常态,与其让下游猜,不如让感知模块明确输出“缺什么、为什么缺、建议怎么问”。追问话术可以由规划层生成,但缺失清单必须由感知层给。这样职责清晰,追问质量也稳定。
第三个技巧是给降级结果打标记并单独监控。降级不可怕,可怕的是降级了没人知道。我会把degraded、confidence、attempt三个字段接入监控,按天看降级率。降级率突然上升,通常意味着输入分布变了或者上游接口出了问题,比等用户投诉要早得多。
5.3 观测与回归:怎么判断它“变笨了”
感知模块的质量不会突然崩,通常是缓慢下滑。输入分布漂移、模型版本更新、提示词微调、检索库膨胀,任何一个因素都可能让准确率掉几个点。没有回归机制,你根本感知不到。
我的做法是维护一套黄金样本,覆盖典型场景和长尾场景,每次改动前后都跑一遍,对比字段级准确率和召回率。样本不用多,一两百条就能看出趋势,但必须定期补充新出现的失败案例。每次线上发现一个典型错误,就把它加进黄金集,这样回归集越来越贴近真实分布。
除了离线回归,线上还要做抽样人工复核。按置信度分层抽样,低置信度多抽,高置信度少抽,每周看一批。复核结果反过来校准阈值和提示词。这套机制跑起来之后,感知模块的迭代就从“拍脑袋改提示词”变成了“有数据支撑的工程优化”。
6. 影响范围:感知模块做好之后,哪些地方跟着变
6.1 对规划层与工具调用的连锁影响
感知模块的输出质量直接决定规划层的输入质量。字段稳定、缺失明确、置信度可信,规划层就能用简单规则做分支,不需要写一堆防御性判断。我做过对比,同一套规划逻辑,接干净感知输出和接脏感知输出,工具调用成功率能差出一大截。
更明显的变化在工具参数上。感知层把实体、约束、时间都结构化好了,工具调用只需要做字段映射,不需要在调用前再让模型解析一遍自然语言。这不仅快,而且可测试。参数错了可以直接定位到是哪个感知字段的问题,而不是笼统地归因于“模型没理解”。
还有一个连带影响是可观测性。感知层输出结构化之后,整条链路的日志都能对齐同一个状态对象,排查时可以从最终结果一路回溯到原始输入,中间每一层都有据可查。这种透明度是智能体系统能不能长期维护的关键。
6.2 不同场景下的感知侧重差异
客服场景最看重意图和情绪,实体抽取反而相对简单,因为业务字段就那么几个。这类场景我会把预算更多放在意图分类和指代消解上,同时把敏感信息过滤做扎实,避免把用户隐私写进长期记忆。
文档审阅场景最看重表格和长文本。分块策略、表格结构还原、跨页对齐是重点,抽取字段要能溯源到具体页码和段落。这类场景对置信度要求高,低置信度宁可转人工,也不要自动通过。
设备巡检类场景最看重图像和时间序列。图片方向、清晰度、拍摄角度影响很大,时间戳要和设备时钟对齐。这类场景的感知模块往往需要和规则引擎配合,模型负责识别,规则负责判断是否越界。
个人助理场景最看重记忆和偏好。用户说“老样子”“按上次的来”,感知层要能快速召回历史状态。这类场景的记忆写入策略要保守,宁可少记,不要记错,否则后面每次召回都是在放大错误。
注意:无论哪个场景,都不要把感知模块做成黑盒。它的输出必须是可读、可校验、可回放的结构化数据,否则一旦出问题,整个系统都会陷入“看起来都对,就是结果不对”的泥潭。
我个人在实际项目里的体会是,感知模块的投入产出比在前期看不出来,越到后期越明显。前期多花两天做归一化和校验,后期能省下两周的排查时间。那些看起来“不够智能”的确定性代码,恰恰是智能体能不能稳定跑下去的地基。如果让我给一个建议,我会说:先把输入归一化和输出契约做扎实,再去调模型和提示词,顺序反了,后面全是返工。