1. 项目概述:为什么电商搜索必须经历这场“认知革命”
我做电商搜索系统落地已经八年,从最早用 MySQL LIKE 模糊查商品标题,到后来搭 Solr 集群扛住双十一流量洪峰,再到去年把整个搜索链路重构成 Search Agent 架构——这三段经历让我彻底明白一件事:电商搜索从来不是“找得到”,而是“猜得准”。标题里说的“从单体到 AI 搜索”,表面看是技术栈升级,实则是搜索系统从“被动响应查询”转向“主动理解意图”的范式迁移。它不只关乎 Solr 或 Elasticsearch 怎么配、微服务怎么拆,更核心的是:当用户输入“送妈妈的生日礼物”,系统该优先返回口红还是按摩仪?当搜索词只有“夏天穿的”,要不要自动补全“连衣裙”“冰丝裤”“防晒帽”?这些决策背后,不再是规则引擎或 TF-IDF 权重表能解决的,而是需要语义理解、多源协同、实时反馈的 AI 能力。
这个进化路径不是凭空画饼。我参与过的三个典型项目印证了它的必然性:第一个是某母婴垂直平台,单体搜索在大促时 QPS 突破 8000 就开始超时,运维半夜改 JVM 参数都压不住;第二个是某综合电商,用微服务把搜索拆成 query 解析、召回、排序、聚合四个服务,但各环节仍靠人工调参,新品上架后搜索曝光率下降 40%;第三个就是现在正在跑的 Search Agent 架构,把“用户搜什么”和“用户真正要什么”之间的鸿沟,用轻量级 LLM + 规则引擎 + 实时行为反馈闭环填平。它不是抛弃 Solr,而是让 Solr 只干它最擅长的事——高效倒排索引检索;也不是推翻微服务,而是让每个微服务变成可插拔的“智能模块”。适合谁来看?如果你正被搜索转化率卡在 2.3% 上不去、被运营反复追问“为什么爆款搜不到”、或者刚接手一个年久失修的单体搜索系统,这篇就是为你写的。它不讲虚概念,只拆真实场景里的每一步怎么做、为什么这么选、踩过哪些坑。
2. 整体架构演进逻辑:单体 → 微服务 → Search Agent 的三次跃迁
2.1 单体搜索的“甜蜜陷阱”与崩溃临界点
早期电商搜索几乎全是单体架构:一个 Java Web 应用,内置 Lucene 或 Solr 嵌入式实例,前端请求进来,经过分词、查询解析、打分排序,最后返回 JSON。这种结构开发快、部署简单,上线三天就能跑通基础搜索。但它的“甜蜜”背后埋着三颗定时炸弹:
第一颗是资源争抢。搜索服务和商品详情页、购物车共用同一套 Tomcat 线程池和 JVM 内存。大促期间搜索 QPS 暴涨,线程池被打满,连带详情页也 503。我见过最极端的案例:某平台在 618 零点,搜索接口耗时从 80ms 拉到 2.3s,而商品详情页同步卡死,客服电话被打爆——根本原因是 Solr 的 IndexWriter 在合并 segment 时占用了全部 CPU,而应用层毫无感知。
第二颗是扩展僵化。想提升吞吐量?只能垂直扩容——换更高配服务器。但 Solr 的 JVM Heap 超过 32GB 后,GC 停顿时间会指数级增长。我们试过把单节点堆内存从 16G 加到 48G,结果 Full GC 平均每次停 8.7 秒,比用户耐心还长。横向扩容?单体架构下 Solr 集群的主从同步延迟、分片路由策略、查询合并逻辑全得自己写,代码量比业务逻辑还多。
第三颗是能力耦合。搜索排序逻辑硬编码在 Java 里,比如“销量权重 * 0.3 + 评分权重 * 0.5 + 新品标签 * 0.2”。运营想临时加个“618 专属权重”,得改代码、走发布流程、重启服务。有次为赶活动,开发连夜改完,测试却漏测了“价格区间筛选失效”的 bug,导致当天搜索 GMV 下跌 12%。
提示:单体搜索的崩溃临界点不是绝对 QPS 值,而是QPS × 平均查询复杂度 × 数据更新频率的乘积。当商品库日增 5 万 SKU、搜索平均词数从 2.1 个升到 3.8 个、且支持同义词/错别字/拼音搜索时,单体架构基本在 QPS 3000 左右就会出现不可控抖动。
2.2 微服务化:拆解物理边界,却暴露了认知断层
微服务改造是多数团队的第一步解药。我们把单体拆成四个核心服务:Query Parser(查询解析)、Recall Service(召回)、Ranking Service(排序)、Aggregation Service(聚合)。每个服务独立部署、独立扩缩容,用 Nacos 做服务发现,Sentinel 控制熔断降级。这套架构在稳定性上立竿见影:召回服务因数据源异常挂了,排序服务仍能返回默认排序结果,用户体验降级而非中断。
但很快暴露出新问题:服务间的数据语义不一致。Query Parser 输出的“用户意图标签”是 {“品类”: “手机”, “价格敏感”: true, “品牌倾向”: “华为”},而 Ranking Service 接收时,发现“价格敏感”字段在协议里定义为布尔值,但实际传过来的是字符串 “true” 或数字 1,导致特征计算全错。更麻烦的是“新品识别”逻辑:Query Parser 认为上架 7 天内是新品,Recall Service 却按库存更新时间判断,结果召回列表里一堆“已售罄新品”,排序再准也没用。
我们曾用 Knife4j 统一 API 文档,用 OpenFeign 强制类型校验,甚至引入 Apache Avro 做序列化——但所有这些,只解决了“数据怎么传”,没解决“数据什么意思”。微服务把物理边界拆干净了,却让语义边界变得更模糊。就像把一台发动机拆成活塞、曲轴、缸体分别外包给三家工厂,每家都造得精密,但装回去发现活塞行程和曲轴扭矩根本不匹配。
注意:微服务不是银弹。如果团队没有统一的领域建模能力,拆得越细,协作成本越高。我们最终在团队内推行“语义契约先行”:所有服务接口设计前,先用 PlantUML 画出领域模型图,明确每个字段的业务含义、取值范围、变更影响,签字确认后才写代码。这多花 2 天设计时间,却省下 3 周联调返工。
2.3 Search Agent:用 AI 作为“语义粘合剂”重构搜索心智
Search Agent 不是又一个微服务,而是搜索系统的“大脑”。它不处理具体数据,只负责协调、决策、学习。我们把它设计成三层结构:
- 感知层(Perception Layer):对接用户行为日志(点击、加购、下单)、实时商品库变更、外部舆情(如某手机品牌突发公关事件),用轻量级 BERT 模型做实时意图分类;
- 决策层(Decision Layer):基于感知层输出,动态选择召回策略(是走 Solr 倒排索引,还是触发向量召回,或是调用第三方比价 API),并生成排序特征权重组合;
- 执行层(Execution Layer):将决策翻译成具体指令,下发给下游微服务。例如:“对 query=‘iPhone15’,启用向量召回(相似商品),关闭价格过滤(用户未提预算),排序权重设为:销量 0.2、好评率 0.5、时效性 0.3”。
关键突破在于Agent 的“可解释性”设计。我们没用黑盒大模型直接生成结果,而是让 Agent 输出决策依据:
{ "query": "iPhone15", "decision_reason": "用户历史 3 次搜索含‘苹果’,最近点击集中在 5000-8000 元价位,故关闭价格过滤", "recall_strategy": "vector_recall", "ranking_weights": {"sales": 0.2, "review_score": 0.5, "freshness": 0.3} }这个 JSON 不仅给下游服务执行,也存入审计日志。运营人员在后台能看到“为什么这个搜索结果这样排”,技术同学能快速定位决策偏差——比如发现“freshness”权重被误设为 0.8,导致老款 iPhone12 排在前面,立刻回滚配置。
实操心得:Search Agent 的价值不在“多智能”,而在“可干预”。我们预留了 3 个干预入口:① 运营手动覆盖决策(如大促期间强制置顶某款);② 算法同学上传新策略包(JSON 格式,无需发版);③ 系统自动熔断(当决策置信度 < 0.6 时,退回到规则引擎兜底)。这保证了 AI 不是取代人,而是放大人的判断力。
3. 核心模块实现细节:Solr、微服务、AI 如何真正协同
3.1 Solr 的“瘦身”实践:从全能选手到专业检索引擎
很多人以为微服务化后 Solr 就该淘汰,其实恰恰相反——它在 Search Agent 架构里变得更重要,只是角色变了。我们把 Solr 从“什么都干”变成“只干检索”,做了三件事:
第一,剥离分词与查询解析。原 Solr 的 schema.xml 里定义了 IK 分词器、同义词库、拼音转换器,导致每次改分词规则都要重启 Solr。现在 Query Parser 服务用 jieba+自定义词典做分词,输出标准 token 流,再拼成 Solr 的q参数。比如用户搜“华为mate60”,Parser 输出q=title:华为 AND title:mate60,而不是让 Solr 自己去切词。这样分词策略升级只需改 Parser 代码,Solr 零感知。
第二,固化召回策略为 Solr 查询模板。我们预定义了 8 种召回模板,存在 Nacos 配置中心:
template_hot:q=category:手机 AND sales:[1000 TO *](热销召回)template_new:q=category:手机 AND publish_time:[NOW-7DAYS TO NOW](新品召回)template_vector:q={!knn f=embedding_vector topK=50}...(向量召回,需 Solr 9.2+)
Agent 根据决策层指令,从配置中心拉取对应模板,填充变量后发请求。模板化让召回策略可灰度、可回滚、可 A/B 测试。
第三,用 Solr 插件实现轻量级重排序。虽然主体排序交给 Ranking Service,但某些低延迟场景(如搜索建议)需要 Solr 内部快速重排。我们开发了一个BoostByClickRatePlugin,在 Solr 的SearchComponent中注入,根据实时 Redis 中的商品点击率(每 5 分钟更新),动态调整boost参数。实测在搜索建议场景,首屏点击率提升 18%,且不影响主搜索链路。
关键参数说明:Solr 的
maxBooleanClauses默认 1024,当用户搜“苹果 香蕉 橙子 葡萄 草莓……”等长尾词时极易触发TooManyClausesException。我们将其调至 32768,并配合 Query Parser 做关键词截断(保留前 8 个有效词),避免异常。这个值不是越大越好——实测超过 65536 后,Solr 查询解析耗时陡增,得不偿失。
3.2 微服务整合实战:Nacos + Sentinel + Knife4j 的黄金三角
微服务不是搭好就完事,关键是让它们“安全地协作”。我们用 Nacos、Sentinel、Knife4j 构成稳定三角,每个组件都针对搜索场景做了深度定制:
Nacos 服务治理:不只是注册中心
- 服务分级:将搜索相关服务标记为
search-group,非搜索服务(如订单、支付)标记为trade-group,避免跨组调用污染链路; - 配置隔离:为不同环境(dev/test/prod)设置独立命名空间,搜索服务的 Solr 地址、Redis 密码等敏感配置绝不混用;
- 健康检查:对 Recall Service 增加自定义健康检查——不仅 ping 端口,还定期发
curl -X GET "http://localhost:8080/health?check=solr",验证 Solr 连通性。当 Solr 不可用时,服务自动下线,避免流量打过去超时。
Sentinel 流量治理:搜索场景的精准熔断
- QPS 限流:对 Query Parser 的
/search接口,按集群维度限流 5000 QPS,单机阈值动态计算(总限流值 ÷ 当前在线实例数); - 熔断降级:当 Ranking Service 的平均 RT 超过 300ms(连续 5 秒),自动熔断 30 秒,期间请求直接返回缓存结果;
- 热点参数限流:针对
category_id做热点限流,防止某类目(如“手机”)突发流量打垮服务。我们用 Sentinel 的ParamFlowRule,对 category_id 设置每秒 200 次调用上限,超出的请求返回“类目热度太高,请稍后再试”。
Knife4j 文档:让协作从“猜”变“看”
- 字段注释强化:在 Swagger 注解中,不仅写
@ApiModelProperty("商品ID"),还补充业务约束:@ApiModelProperty("商品ID,64位字符串,全局唯一,格式:sku_开头+16位随机码"); - 示例值驱动:每个接口提供 3 个真实场景示例:正常搜索、空结果搜索、错误参数搜索,附带返回体和状态码;
- 权限标识:在文档中标明接口权限等级,如
@ApiOperation(value = "搜索建议", author = "search-team", tags = {"public"}),避免前端误调用内部接口。
实操心得:Knife4j 的
@ApiIgnore别乱用!我们曾为“隐藏内部调试接口”加了这个注解,结果测试同学不知道接口存在,漏测了关键路径。后来改成统一前缀/internal/,并在 Knife4j 配置中过滤该前缀,既隐藏又留痕。
3.3 Search Agent 的 AI 能力落地:轻量模型 + 规则引擎 + 实时反馈
Search Agent 的 AI 不是堆算力,而是“小步快跑”。我们选型原则很明确:能用规则解决的,绝不用模型;能用小模型解决的,绝不用大模型。具体实现分三层:
感知层:BERT-Base 微调 + 行为日志流处理
- 模型选型:放弃 LLaMA 或 Qwen,用 HuggingFace 的
bert-base-chinese,在自有搜索日志上微调。训练数据是 100 万条 query-click pair,标签是 5 类意图:{“找商品”, “比价格”, “查参数”, “看评价”, “找活动”}; - 实时处理:用 Flink 消费 Kafka 中的用户行为日志,每 10 秒窗口统计用户最近点击的品类分布、平均停留时长、加购率。这些指标和 query 一起喂给 BERT 模型,输出意图概率。比如搜“戴尔笔记本”,若用户最近 3 次点击都是“游戏本”,模型会高置信度输出 {“intent”: “找商品”, “sub_intent”: “游戏本”};
- 成本控制:模型部署用 ONNX Runtime,单实例 QPS 达 1200,GPU 显存占用仅 1.2GB,比 PyTorch 原生部署省 60% 资源。
决策层:规则引擎 + 策略编排
- 规则引擎:用 Drools,但做了搜索定制。定义规则时,条件部分支持实时变量:
rule "新品优先" when $q: Query(intent == "找商品", subIntent == "新品") $p: Product(publishTime > now.minusDays(7)) then modify($p) { setBoost(2.0) } end - 策略编排:Agent 启动时加载策略包(JSON 格式),包含召回策略、排序权重、熔断阈值。策略包版本号与 Git Tag 对齐,回滚就是切版本号,5 秒生效。
执行层:指令化 API + 审计追踪
- 指令格式:Agent 不返回商品列表,只返回执行指令:
{ "command": "RECALL", "strategy": "template_hot", "params": {"category": "手机", "min_sales": 500}, "timeout_ms": 200 } - 审计追踪:每条指令生成唯一 trace_id,记录下发时间、下游服务响应、实际执行耗时。当某次搜索结果异常,运营输入 trace_id,5 秒内查到是 Ranking Service 的某个特征计算超时,而非 Agent 决策错误。
关键经验:AI 模型上线前必须过“三关”:①离线关:在历史数据上 AUC ≥ 0.85;②灰度关:1% 流量走 AI,对比规则引擎的 CTR、GMV;③熔断关:设置置信度阈值(如 0.7),低于此值自动切回规则引擎。我们曾因灰度期没设熔断,某次模型误判“苹果”为水果类目,导致 iPhone 搜索结果里出现苹果手机壳和红富士苹果,紧急回滚。
4. 实操全流程:从零搭建 Search Agent 搜索系统
4.1 环境准备与基础组件部署
部署不是复制粘贴,而是理解每个组件的“生存逻辑”。我们按搜索链路依赖顺序部署,确保上游组件就绪后才启动下游:
第一步:Nacos 高可用集群(3 节点)
- 服务器:3 台 4C8G,磁盘 100GB SSD;
- 部署命令:
# 下载 nacos-server-2.2.3.tar.gz,解压后修改 conf/application.properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://10.0.1.10:3306/nacos?charset=utf8mb4&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=your_password # 启动:./bin/startup.sh -m standalone # 测试环境单机启动 # 生产环境用集群模式:./bin/startup.sh -p embedded - 关键配置:
nacos.core.auth.enabled=true开启鉴权,nacos.core.auth.plugin.nacos.token.secret.key设为强密码,避免未授权访问。
第二步:Solr 9.2 集群(2 主 2 从)
- 服务器:4 台 8C16G,SSD 磁盘,JVM 参数
-Xms8g -Xmx8g -XX:+UseG1GC; - 配置要点:
solrconfig.xml中<luceneMatchVersion>LUCENE_9_0</luceneMatchVersion>必须匹配;managed-schema里定义text_general字段类型,禁用copyField(避免冗余存储);- 启用
solr.log日志级别设为WARN,避免海量 INFO 日志拖慢磁盘 IO。
第三步:Sentinel 控制台与客户端
- 控制台部署:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar sentinel-dashboard-1.8.6.jar; - 服务端接入:在每个微服务的
pom.xml加依赖:<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-client</artifactId> <version>2.2.9.RELEASE</version> </dependency> - 关键配置:
spring.cloud.sentinel.transport.dashboard=localhost:8080,spring.cloud.sentinel.eager=true(启动即连接)。
注意:Nacos 和 Solr 的 ZooKeeper 不能共用!我们曾为省资源,让 Solr 复用 Nacos 的 ZooKeeper,结果 Nacos 重启时 Solr 集群脑裂,数据不一致。后来给 Solr 单独部署 3 节点 ZooKeeper,虽多 3 台机器,但稳定性提升 100%。
4.2 微服务模块开发与集成
每个服务都遵循“最小职责”原则,代码结构高度统一:
Query Parser 服务(Spring Boot 2.7)
- 核心逻辑:
@RestController public class SearchController { @PostMapping("/parse") public ParseResult parse(@RequestBody QueryRequest request) { // 1. 敏感词过滤(调用内部风控服务) String cleanQuery = riskService.filter(request.getQuery()); // 2. 分词(jieba + 自定义词典) List<String> tokens = jiebaSegmenter.segment(cleanQuery); // 3. 意图识别(调用 Search Agent 感知层 API) Intent intent = agentClient.perceive(tokens, request.getUserId()); // 4. 生成 Solr 查询语句 String solrQuery = solrQueryBuilder.build(intent, tokens); return new ParseResult(solrQuery, intent); } } - 关键点:
riskService.filter()是同步调用,超时设为 200ms,失败则跳过,不阻塞主流程。
Recall Service(Go 1.19,高性能召回)
- 用 Go 是因为其并发模型天然适配多数据源召回:
func Recall(ctx context.Context, req *RecallRequest) (*RecallResponse, error) { // 启动 goroutine 并行调用 Solr、向量库、MySQL var wg sync.WaitGroup ch := make(chan *RecallResult, 3) wg.Add(1) go func() { defer wg.Done(); ch <- solrRecall(req) }() wg.Add(1) go func() { defer wg.Done(); ch <- vectorRecall(req) }() wg.Add(1) go func() { defer wg.Done(); ch <- mysqlRecall(req) }() go func() { wg.Wait(); close(ch) }() // 汇总结果,去重合并 results := mergeResults(ch) return &RecallResponse{Items: results}, nil } - 性能实测:单实例 QPS 1200,P99 耗时 180ms,比 Java 版本低 40%。
Ranking Service(Python 3.9 + LightGBM)
- 特征工程:从 Redis 实时读取用户画像(最近 3 天点击品类、平均客单价),从 MySQL 读取商品静态特征(类目、品牌、评分);
- 模型训练:用 LightGBM,特征重要性排序前三是:
user_click_rate(用户对该类目点击率)、item_review_score(商品评分)、category_sales_rank(类目内销量排名); - 部署:用 Flask + Gunicorn,模型文件
.txt格式加载,启动时预热,避免首次请求冷加载。
实操心得:Go 写 Recall Service 时,一定要用
context.WithTimeout控制每个数据源调用超时。我们曾因向量库响应慢(3s),导致整个召回耗时飙升,后来给每个 goroutine 加ctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond),超时自动放弃该路召回,保障整体 P99 稳定。
4.3 Search Agent 部署与策略配置
Agent 是系统“大脑”,部署必须稳、准、快:
部署步骤:
- 拉取镜像:
docker pull search-agent:v1.2.0(镜像含 ONNX Runtime + Drools + Kafka Client); - 创建配置:
config.yaml包含 Kafka 地址、Nacos 地址、模型路径、策略包 URL; - 启动容器:
docker run -d \ --name search-agent \ -v /path/to/config.yaml:/app/config.yaml \ -v /path/to/models:/app/models \ -p 8081:8081 \ --restart=always \ search-agent:v1.2.0
策略包配置(JSON 格式):
{ "version": "20240520", "strategies": [ { "name": "default_search", "recall": ["template_hot", "template_new"], "ranking_weights": { "sales": 0.25, "review_score": 0.4, "freshness": 0.15, "brand_power": 0.2 }, "fallback_rule": "rule_simple_sort" } ], "rules": [ { "name": "rule_simple_sort", "condition": "intent == '找商品'", "action": "sort_by_sales_desc" } ] }- 策略包上传到 Nacos 的
dataId=search-agent-strategy,Agent 启动时自动拉取,变更时监听配置更新。
上线验证 checklist:
- [ ] Agent 能正常消费 Kafka 用户行为日志(查看日志
INFO - Flink job started); - [ ] 模型推理接口
POST /agent/perceive返回{"intent":"找商品","confidence":0.92}; - [ ] 策略包加载成功(日志
INFO - Loaded strategy version 20240520); - [ ] 执行指令下发到 Recall Service(用 Wireshark 抓包验证 HTTP 请求)。
关键技巧:策略包版本管理用 Git Tag,不是时间戳。
git tag -a v1.2.0 -m "618大促策略",然后 CI/CD 自动打包上传。这样回滚时git checkout v1.1.0即可,避免时间戳冲突。
5. 常见问题排查与避坑指南:血泪总结的 12 个实战陷阱
5.1 Solr 相关问题速查
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| Solr 查询偶尔超时,但监控显示 CPU/内存正常 | JVM GC 频繁,但监控未捕获短时 GC | 1.jstat -gc <pid>查看 YGC 次数/耗时;2.jstack <pid>看线程是否卡在IndexWriter | 调整mergeFactor(默认 10)为 5,减少 segment 合并频率;增加maxMergeMBForOptimize |
| 搜索结果出现重复商品 | Solr 分片路由策略错误,同一商品被索引到多个分片 | 1.curl "http://solr:8983/solr/collection/select?q=id:SKU12345"查各分片;2. 检查router.field配置 | 改用compositeId路由,确保相同id落在同一分片;或启用distrib=false强制单分片查询 |
| 高亮显示错乱(如“苹果”高亮成“苹”和“果”) | 分词器与高亮器不匹配 | 1.curl "http://solr:8983/solr/collection/analysis/field?analysis.fieldvalue=苹果手机&analysis.fieldname=title"查分词结果;2. 对比hl.fl字段的分词器 | 统一高亮字段的analyzer和queryAnalyzer,或改用UnifiedHighlighter |
踩坑实录:我们曾因 Solr 的
hl.fragsize设为 100,导致长商品描述被截断,用户看不到完整信息。后来改成hl.fragsize=0(不限制片段长度),用hl.maxAnalyzedChars=10000控制分析字符数,既保全文又防 OOM。
5.2 微服务通信故障诊断
问题:Recall Service 调用 Solr 偶发 500,但 Solr 日志无报错
- 排查:用
tcpdump抓包发现,Recall 发送的 HTTP 请求头Content-Length为 0,但 body 有数据,Solr 拒绝解析; - 原因:Recall 用 OkHttp,未显式设置
Content-Length,而 Solr 的 Jetty 版本较老,严格校验; - 解决:OkHttp 添加拦截器,强制计算并设置
Content-Length。
问题:Sentinel 限流不生效,QPS 超过阈值仍放行
- 排查:
curl http://localhost:8080/actuator/sentinel查流控规则,发现resource名称与代码中@SentinelResource("search:query")不一致; - 原因:Spring Cloud Alibaba Sentinel 默认资源名是
HTTP_METHOD:URL,如GET:/search,而注解指定了自定义名; - 解决:统一用
@SentinelResource("search_query"),并在 Sentinel 控制台创建同名规则。
问题:Knife4j 文档中枚举值显示为ENUM_1, ENUM_2,而非热销, 新品
- 排查:Swagger 的
@ApiModel未加@ApiModelProperty的example属性; - 解决:在枚举类上加
@ApiEnum注解,或在 Controller 方法参数上用@ApiParam(example = "热销")。
5.3 Search Agent 决策异常处理
问题:Agent 对同一 query 连续两次决策不同(如第一次召回 hot,第二次召回 new)
- 排查:查 Kafka 日志,发现用户行为流有延迟,Agent 第一次收到旧行为数据,第二次收到新数据;
- 解决:在 Flink 中加
watermark延迟 30 秒,确保行为数据按事件时间有序;Agent 决策时加event_time时间戳,丢弃延迟超 60 秒的数据。
问题:模型置信度突然暴跌(从 0.9 降到 0.3),但模型文件未更新
- 排查:查模型输入,发现 Query Parser 输出的 token 流中混入了 HTML 标签(如
<script>),模型从未见过; - 解决:在 Parser 中加
Jsoup.clean(query, Whitelist.none())过滤所有 HTML 标签,只留纯文本。
问题:策略包更新后,Agent 未生效
- 排查:
curl http://agent:8081/actuator/env查配置,发现nacos.config.group配置错误,读取了 test 环境的策略; - 解决:统一用
nacos.config.group=SEARCH_GROUP,并在 Nacos 中严格按 group 管理配置。
最后分享一个小技巧:给 Search Agent 加个
/debug/decision接口,输入 query 和 user_id,返回完整决策链路(包括感知层输出、规则匹配过程、最终指令)。这个接口不开给前端,只给运维和算法同学用,排查问题效率提升 70%。上线三个月,我们靠它定位了 90% 的决策异常,比翻日志快得多。