📚 Harness 学习计划 / 第1周 / Day02_通读AI搜索链路

Day 2 · 通读项目 AI 搜索完整链路

属性
日期 2026-08-13(周四)
周次 第1周:双轨认知框架建立
轨2 · 项目 Agent 落地调研
时间 1 小时
状态 ✅ 已完成

📋 Checklist

📖 阅读材料

材料 路径
前端 AI 搜索首页 smallrig-shop-mall/pc/src/pages/ai-search/index.vue
前端 AI 搜索结果 smallrig-shop-mall/pc/src/pages/ai-search/result.vue
后端搜索服务 smallrig-mall-product/.../search/impl/SearchServiceImpl.java
OpenAI 工具类 smallrig-mall-plugin-starter/.../util/OpenAiUtils.java
返回数据结构 smallrig-mall-common/.../response/.../AiSearchProductResp.java
前端 M 端 AI 搜索 smallrig-shop-mall/mobile/src/pages/ai-search/

✍️ 学习笔记

在此记录你的笔记...

数据流图

第 1 步:前端
用户输入 "A7III 兔笼" → POST /webapi/search/aiSearchProduct
请求体:{aiKeyWorks: ["A7III 兔笼"], pageSize: 20}

第 2 步:后端 SearchServiceImpl.aiSearchProduct()
  │
  ├─ 2a. 调用 OpenAI Chat API(意图分析)
  │      System Prompt 让模型分析用户意图
  │      模型返回 JSON:{"searchTerm": "A7III cage", "aiReply": "为你找到..."}
  │      关键代码:OpenAiUtils.searchAnalyzeIntent() → line 230
  │
  ├─ 2b. 调用 OpenAI Embedding API(文本转向量)
  │      把 searchTerm 转成向量,用于 ES 语义搜索
  │      关键代码:OpenAiUtils.textToVector() → line 51
  │
  ├─ 2c. Elasticsearch 向量搜索
  │      用向量匹配商品库,返回相似商品列表
  │
  └─ 2d. 组装返回
         AiSearchProductResp {
           searchTerm: "A7III cage",
           aiReply: "为你找到适配 A7III 的配件...",
           productList: [{id:101, name:"全包兔笼",...}, ...]
         }

第 3 步:前端渲染
  AiSearchResultReply 组件展示 aiReply 文本
  ProductBaseCard 组件展示商品卡片列表

当前 AI 搜索的局限性(为什么它不是 Agent?)

用 Day 1 学到的 Agent 框架衡量:

Agent 核心要素 当前 AI 搜索 差距
模型自主决策 ❌ 没有。每次都调同一个 API,模型不判断要不要调 差一步
tool_call 循环 ❌ 没有。模型只输出一次 JSON(searchTerm + aiReply),不是 tool_call 格式 差一步
多轮对话 ❌ 没有。用户输入 → 一次返回 → 结束。没有追问、没有多轮 差一步
tool_response ❌ 没有对应的概念。后端把 AI 返回的 searchTerm 当向量搜索的关键词,但这不是 tool_call/tool_response 循环 格式不同

本质区别:当前 AI 搜索是「把 AI 当翻译器用」——用户输入 → AI 提取关键词 + 生成文案 → 后端用关键词搜商品。AI 没有决策权,它只是管线里的一个环节。

对比图

当前 AI 搜索:
user → [AI 意图分析] → [向量搜索] → [返回结果] → 展示
        ↑ 一次调用,AI 只是管线里的一个步骤

真正的 Agent:
user → 模型判断 → tool_call → 系统执行 → tool_response → 模型再判断 → 回复
        ↑ 模型是决策中枢,自主决定调不调、调哪个、调几次

❓ 疑问

📤 产出


🔧 Agent 改造方案:当前代码 vs Agent 格式

第一步:定义 Tool(System Prompt 里新增)

<tools>
[
  {
    "name": "search_product",
    "description": "搜索 SmallRig 商品库,按关键词和品类查找商品",
    "parameters": {
      "query": "搜索关键词,如 'A7III cage'",
      "category": "商品品类:cage/tripod/monitor/mic/other",
      "max_price": "最高预算(可选数字)",
      "language": "语言代码:en/de/ja/ko/zh"
    }
  }
]
</tools>

第二步:模型输出 tool_call(替代当前代码的 searchTerm)

<|im_start|>assistant
<tool_call>
{
  "name": "search_product",
  "arguments": {
    "query": "A7III cage",
    "category": "cage",
    "max_price": null,
    "language": "en"
  }
}
</tool_call>
<|im_end|>

对比当前代码:这替代了 OpenAiUtils.searchAnalyzeIntent() 返回的 searchTerm 字段。但区别是——这是模型自主决定要调用 search_product 工具,而不是后端硬编码的管线。

第三步:后端路由到 handler(替代当前代码的硬编码搜索)

// 当前代码:SearchServiceImpl.line 227
List<ProductSearchResp> prdList = searchEsProductList(...);

// 改造后:这个函数变成 search_product 这个 Tool 的 handler
// 后端收到 tool_call 时,根据 name="search_product" 路由到这里

第四步:后端返回 tool_response(替代当前代码的 productList)

<|im_start|>tool
<tool_response>
{
  "name": "search_product",
  "content": {
    "products": [
      {"id": 101, "name": "A7III Full Cage", "price": 899},
      {"id": 102, "name": "A7III Half Cage", "price": 599}
    ],
    "total": 2,
    "query": "A7III cage"
  }
}
</tool_response>
<|im_end|>

对比当前代码:这替代了 AiSearchProductRespproductList 字段。但区别是——这是 Tool 执行结果的通用格式,不绑定到特定的搜索 API。

第五步:模型读 tool_response,生成最终回复(替代当前代码的 aiReply)

<|im_start|>assistant
I found 2 cages compatible with your Sony A7III, both within your budget:

1. **A7III Full Cage** - $899
   Full cage design, better protection for professional use.

2. **A7III Half Cage** - $599
   Half cage design, lightweight and portable.

Would you like me to compare the detailed specs of these two?
<|im_end|>

对比当前代码:这替代了 OpenAiUtils.searchAnalyzeIntent() 返回的 aiReply 字段。区别是——这是模型读了真实搜索结果后生成的回答,而不是在搜索前就提前生成的文案。

一张图看懂改造前后

改造前(当前代码):
                 ┌───────────────────────┐
                 │  一次 API 调用          │
用户输入 ───────→│  OpenAI 分析意图        │──────→ searchTerm → ES 搜索
                 │  返回 searchTerm        │        aiReply → 直接展示
                 │  + aiReply             │
                 └───────────────────────┘
                 ↑ 模型不知道后面有搜索     ↑ aiReply 是提前生成的,不是基于真实结果

改造后(Agent):
                          ┌─────────────────┐
用户输入 ────────────────→│  模型判断         │
                          │  需要查商品吗?   │
                          └────────┬────────┘
                                   │ 是
                                   ↓
                          ┌─────────────────┐
                          │  tool_call       │──────→ 后端执行 search_product
                          │  search_product  │        ES 搜索(和当前代码一样)
                          └─────────────────┘
                                   │
                                   ↓
                          ┌─────────────────┐
                          │  tool_response   │←────── 返回真实商品列表
                          │  {products: [...]}│
                          └─────────────────┘
                                   │
                                   ↓
                          ┌─────────────────┐
                          │  模型读结果,      │
                          │  生成最终回复      │──────→ 用户看到的结果
                          └─────────────────┘
                          ↑ aiReply 是基于真实搜索结果的

核心差异总结

维度 当前代码 Agent 改造后
模型输出 {searchTerm, aiReply} <tool_call>{"name":"search_product","arguments":{...}}
搜索由谁触发 后端硬编码(管线) 模型自主决策(Tool Call)
搜索时机 和 AI 调用同时发生 模型决定后才发生
aiReply 生成时机 搜索前(提前生成文案) 搜索后(基于真实结果生成)
后端代码 写死管线,无法复用 通用 Tool 路由,任何 Tool 走同一套逻辑

最关键的一句话:当前代码的 searchTermaiReply 是在一次 API 调用里同时生成的,模型「猜」用户会搜到什么。Agent 改造后,模型先决策、再搜索、再回复——aiReply 是基于真实搜索结果的,不是猜的。


🛡️ 异常处理:模型参数不准确怎么办?

三层机制

第一层:参数描述里的值是示意,不是约束

"category": "商品品类:cage/tripod/monitor/mic/other"

这里的 cage/tripod/monitor/mic/other给模型看的提示,告诉它「品类大概长这样」,但模型不一定会严格按这个列表取值。模型可能输出:

✅ "category": "cage"         ← 在提示范围内
✅ "category": "tripod"       ← 在提示范围内
⚠️ "category": "stabilizer"   ← 不在提示范围内,但模型觉得合理
⚠️ "category": ""             ← 模型觉得不需要限定品类

第二层:后端不做判断,只如实返回结果

模型输出:tool_call { "category": "stabilizer" }
           ↓
后端 handler 执行:SELECT * FROM products WHERE category = "stabilizer"
           ↓
结果:返回 0 条数据
           ↓
tool_response:{ "products": [], "total": 0, "message": "未找到匹配商品" }

后端不管 category 值是否合理,只管执行。查不到就返回空结果。

第三层:模型读空结果后,自己决定怎么办

tool_response:{ "products": [], "total": 0 }

模型读后判断:
  "stabilizer 品类没搜到结果,可能这个品类名不对。
   我应该换个方式搜索——不用品类限制,直接用关键词搜。"

模型再次输出 tool_call:
  {
    "name": "search_product",
    "arguments": {
      "query": "stabilizer 稳定器",
      "category": "",           ← 这次不限定品类
      "max_price": null
    }
  }

后端再次搜索 → 返回 5 条结果
模型读结果 → 生成最终回复

核心认知:后端只负责执行,不负责判断。查不到就返回空,模型自己决定要不要重试、换参数、换工具。这就是 Agent 和传统管线最大的区别——管线里异常是 bug,Agent 里异常是模型自主处理的一环。


🧪 Day 2 自测题

第 1 题:链路识别

当前 AI 搜索的流程中,模型(OpenAI)扮演了什么角色?

A. 决策中枢——自主判断要不要调用工具、调哪个
B. 翻译器——把用户输入转成关键词 + 生成文案
C. 数据库——返回商品列表

点击看答案

B。 当前流程中,模型只是把用户输入翻译成 searchTerm 和 aiReply,没有决策权。它只是管线里的一个环节,不是 Agent 里的决策中枢。


第 2 题:对比判断

以下哪个是当前 AI 搜索和 Agent 的核心区别?

A. 当前 AI 搜索用了 OpenAI,Agent 只能用 Hermes
B. 当前 AI 搜索是单次调用,Agent 有模型决策 → tool_call → tool_response 的循环
C. 当前 AI 搜索没有前端界面,Agent 有聊天界面

点击看答案

B。 核心区别是「有没有模型自主决策的循环」。当前 AI 搜索是「用户输入 → 一次 API 调用 → 返回」,Agent 是「模型判断 → 调工具 → 读结果 → 再判断」的循环。


第 3 题:代码定位

SearchServiceImpl.javaaiSearchProduct() 方法中,哪一步对应了「AI 意图分析」?

点击看答案
// 第 213 行
AiSearchDto aiSearchDto = this.buildVectorQueryParam(req);
// 第 370 行(buildVectorQueryParam 内部)
AiSearchDto aiSearchDto = OpenAiUtils.searchAnalyzeIntent(...)

这个方法调用了 OpenAiUtils.searchAnalyzeIntent(),后者调用 OpenAI Chat API,让模型分析用户意图,返回 {searchTerm, aiReply}


第 4 题:改造思路

如果要把当前 AI 搜索改造成 Agent,第一步应该做什么?

点击看答案

第一步:把模型输出从 {searchTerm, aiReply} 改成 <tool_call> 格式。

当前模型输出的是「翻译结果」(关键词 + 文案),Agent 模型应该输出的是「决策指令」(我要调用 search_product 工具,参数是...)。这样后端就有了一个通用的 Tool 路由机制,而不是写死管线。


⏭️ 完成后:更新 当前进度 为 Day 3