面试知识库

M4 · 九大工具 —— 开发文档(面试向)#

讲为什么这么做、做了哪些取舍,不讲代码。读完能用大白话讲出来。

一句话概括#

前面 M1 搭好了 Agent 的「循环骨架」,M2 给了它「分身」能力,M3 造好了「召回弹药库」。M4 干的事是:把 Agent 真正能动手干活的「九件工具」全部造出来并装进工具箱。从此 Agent 不再是空有循环、无事可做,而是手里有了拆需求、搜商品、比价、算到手价、精挑、收尾这一整套家伙事。

打个比方:前三个里程碑是把厨房、灶台、食材都备齐了,M4 是把菜刀、炒锅、漏勺、汤勺一件件打出来挂上墙——下个里程碑(主链路组装)才是真正开火炒菜。


1. 九件工具是怎么分工的#

把一次购物从「用户一句话」到「带理由的清单」拆成一条流水线,每件工具管一段:

  • planner(拆需求):把「便宜抗造的旅行三件套,预算300,不要塑料,喜欢小众」这种大白话,拆成预算、品类、硬约束、软偏好这些结构化字段
  • item_search(搜货):在单个平台里按语义搜商品。
  • price_compare(比价):把各平台不同币种的价格统一折成美元再排序。
  • shipping_calc(算到手价):货价之外再加上运费和关税,算出真正「到手多少钱」。
  • item_picker(精挑):在一堆候选里按用户偏好挑出最终几件,并写明为什么选它。
  • category_insight(品类洞察):搜之前先摸底——这个品类的爆款、价位段、常见品牌长啥样。
  • web_search(查外部):商品库里没有的信息(评测、博主推荐、价格趋势)上公网查。
  • chat_fallback(闲聊兜底):用户这句压根不是买东西,就友好回一句收尾。
  • shopping_summary(收尾出清单):拿到精选结果,生成最终清单 + 每件的选购理由,顺手把用户新偏好记下来。

面试怎么讲:九件工具不是平铺的功能列表,是一条「意图 → 检索 → 比价 → 算钱 → 精挑 → 收尾」的流水线,每件工具单一职责、各管一段。Agent 的活就是在循环里决定「现在该抄哪把刀」。


2. 最关键的一个判断:哪些工具用大模型,哪些不用#

九件工具里,我只让三件用大模型(planner、chat_fallback、shopping_summary),其余六件全是确定性的纯代码。这个划分是 M4 最重要的设计决定。

判断标准很朴素:这件事需不需要「理解语言」或「生成语言」?

  • 需要的——拆解一句模糊的中文意图、写一段得体的回复、把候选润色成带理由的清单——这是大模型的主场,给它。
  • 不需要的——折算汇率、加运费关税、按价格排序、按关键词过滤——这些是确定的算术和规则,写死的代码又快、又准、又能被测试逐位断言。硬塞给大模型反而引入幻觉(它可能把汇率算错)、花冤枉钱、还测不稳。

这个划分带来一个很实在的好处:六件确定性工具的测试完全离线、零网络、可复现。比价工具喂进去 100 美元 + 50 欧元,我能精确断言输出是 100 和 54,差一分都报错。只有那三件大模型工具的测试需要「假模型」替身。

面试怎么讲:我没有「能用大模型就用大模型」。规则:要理解/生成语言的才给大模型,剩下能用算术和规则说清楚的一律写死。换来的是确定性、可测性、省钱——比价这种事让大模型算,错了都不知道,写死的代码错了测试当场红。


3. 候选商品如何一路「渐进填充」#

流水线上五件工具要传递「候选商品」。我没给每件工具各定一套输入输出格式,而是用同一个商品结构贯穿全程,让它一路被补全

  • item_search 召回时,只填得起基础字段(标题、原价、原币种)。
  • price_compare 折算后,补上「美元价」。
  • shipping_calc 算完,补上「运费、关税、到手价」。
  • item_picker 选完,补上「入选理由」。

哪个阶段还没做,对应字段就空着。这样大模型在循环里读到候选时,字段名从头到尾一致,一眼能看出「这件货算到哪一步了」,不用在每件工具的输出之间反复对照换算。

面试怎么讲:候选商品是流水线上的「半成品」,每过一道工序补一道字段,而不是每道工序换一种包装。好处是字段稳定、省 token、大模型不容易搞混;代价是这个结构会比较「胖」,带一堆可空字段——但比起让模型在五种格式间脑补映射,这个代价值得。


4. 「单平台搜索 + fork 并行」:为什么不让一个工具搜全平台#

item_search 一次只搜一个平台。要跨六个平台搜,不是在工具里写个循环,而是靠 M2 做的 fork——主 Agent 派六个分身,每个分身搜一个平台,同时跑

为什么这么设计?因为「要不要并行」这个决策权,应该留在主循环手里,而不是焊死在工具里。跨平台是天然能并行的活(六个平台互不依赖),正好命中 fork 三件事里的「能并行」。工具只管「搜好一个平台」这件最小的事,并行编排交给上层——这样工具简单、可单独测,主循环也能灵活决定这次到底搜几个平台。

面试怎么讲:我故意把 item_search 做成单平台。跨平台并行不是工具的职责,是主循环的调度职责——用 M2 的 fork 来做。工具保持单一最小职责,并行与否上层说了算,这比在工具里写死「搜全部平台」灵活得多。


5. 两个「收尾出口」:怎么治 Agent 不收尾的老毛病#

Agent 最经典的失败是不收尾、反复调工具死循环。M4 在工具层留了两个明确的「话讲完了」出口,标记为终结性工具——一旦调用,主循环立即停:

  • shopping_summary:正常买完东西的收尾。
  • chat_fallback:用户压根没在买东西的收尾。

给模型两个清晰的「下班打卡点」,配合系统提示词里把「信息够了就立刻收尾」拔到最高优先级,是对治死循环最直接的一手。

面试怎么讲:不收尾是 Agent 头号顽疾。我的解法是给它明确的退出门——两个终结性工具,调了就停。光靠提示词喊「别死循环」不够,得在工具层给它实打实的出口。


6. 几个「诚实的桩」:缺东西也要能跑#

M4 是中间里程碑,有些下游依赖还没就位。我的原则是缺啥都不让链路崩,而是优雅降级 + 诚实标注:

  • category_insight 是个桩:真正的品类知识库(独立语料 + 专门的混合检索 + 精排)是下一个里程碑的事。现在我用已有的商品索引就地聚合出洞察——把品类当搜索词捞一批同类货,再用规则算出价位段、热门品牌、爆款。返回里特意标了 local_index_stub,明说这是桩,不冒充真知识库。形对了、数据源先顶着,下个里程碑把数据源一换就行,工具签名不动。
  • web_search 会优雅降级:没配外部搜索的密钥时,它不报错不崩,而是返回空结果 + 一句说明,让主循环知道「这条外部信息暂时拿不到」,照样能基于已有信息收尾。这跟 M3 里「编码器连不上就退本地」是同一套思路。
  • 个性化通道先空转:item_search 保留了「个性化」这一路(用用户长期偏好影响搜索结果),但驱动它的偏好数据要等长期记忆那个里程碑才有。现在不传就退化成纯语义搜索,位置留着,数据后补。

面试怎么讲:中间里程碑最忌「这块没好所以整条跑不起来」。我让每个未就绪的依赖都能降级——桩、空结果、纯语义兜底——并且诚实标注哪是桩。这样链路任何时候都是通的,下游一就位就无缝替换。


7. 一个差点埋雷的数据坑:缺三个币种#

动手前我对着真实数据做了一遍核查,揪出一个确定性的坑:比价要把各平台币种折成美元,但汇率表里缺墨西哥比索、智利比索、哥伦比亚比索这三个拉美币种——而它们恰恰是 shopee 平台的主力报价币,占了那批数据的六成。不补的话,一搜 shopee 就大面积「未知币种」崩溃。

补法很简单(加三行近似汇率),但这件事的价值在「先核查再动手」:如果闷头写完工具才发现,早就埋进链路里了,调试起来还得回溯到数据层。

顺手还把这个坑变成了一条通用的容错规则:折算时遇到任何不认识的币种,不抛错崩整批,而是把那一条标记成「折不动」沉到末尾,让模型自己判断。一条脏数据不该掀翻整次比价。

面试怎么讲:我有个习惯——动手前先拿真实数据核查工具的前置依赖。这次就抓到汇率表缺 shopee 六成订单的币种。补三行是小事,但「先核查」让它在写代码前就被堵掉,而不是上线后线上崩。


8. 代码 review 抓出并修掉的几处#

按流程过了一轮高强度 review,修了四处、也有意识地放过四处(诚实记下来):

修了:

  • 「截断」信号撒谎:item_search 原本「取满 K 条就报『被截断』」,但「恰好取满」和「索引本就只有这些」是两回事。改成多取一条探一下——确实还有更多才报截断,否则如实说「这就是全部」。
  • 没人评分 ≠ 评分极低:item_picker 打分时,把「还没人评过」的商品当成「评分 0 分」来罚,会把新上架的好货埋掉。改成给个中性分,跟「价格未知给中性分」的处理对齐。
  • 重复代码收口:「折美元、折不动就跳过」这段逻辑在三处各抄了一遍,抽成一个公共小函数。
  • 事件上报漏补:三件大模型工具如果模型调用中途失败,「工具结束」事件就不发了——前端会看到工具「永远在转」。补成无论成功失败都补一条结束事件。

有意识放过的(附理由):

  • category_insight 在样本极少时置信度偏低、价位段可能三档重合——这是桩的固有局限,下个里程碑换真知识库就没了,现在没必要为桩过度打磨。
  • item_picker 对「价格未知」的商品在预算筛选时放行——这是个判断题,而「价格未知」只在「币种不认识」时发生、本就罕见,保留更不容易误杀。
  • 召回客户端做成进程内单例可能让测试间索引串味——但我的测试是直接替换掉工具里的客户端引用、不走单例,全量测试一次跑过、无实际抖动;而单例在生产里正是想要的行为。

面试怎么讲:review 不是把所有「可以更好」的地方全改一遍。真 bug(撒谎的截断信号、误罚无评分商品)当场修;属于「桩的局限」「判断题」「无实际影响」的,我会写清理由放过,避免为了消除告警而过度工程。


9. 后续演进(M4 之后陆续落地的增强)#

M4 造好了九件工具、装进工具箱。后续里程碑在不改变工具分工的前提下,叠加了几项增强:

  • 工具思考结果摘要:每件工具的 report_tool_end 事件现在带一个 result 字段,是一段人读摘要——planner 输出拆解后的品类/预算/偏好、item_search 输出 Top 候选标题、price_compare 输出归一价排序、shipping_calc 输出到手价、item_picker 输出精选理由、category_insight 输出爆款/价位/维度、web_search 输出 Tavily 摘要。前端「思考过程」展开看的是这一步查到/想出了什么,而非 card_count=8 这类元信息。
  • item_search 多维 filter:新增 price_usd_max(预算上限)、min_rating(最低评分)、brand_exclude(排除品牌)三个参数,在 Qdrant 召回阶段走 payload filter,减少召回一堆超预算/低分商品再在精挑阶段扔掉的浪费。
  • web_search 门控放宽:从「仅在 item_search 召回为空时作兜底」改为两种合法场景:① 独立知识查询(用户问品牌口碑/评测/对比、还没进入购物检索流程)直接放行;② 购物流程中 item_search 全空时兜底放行。已有候选时仍拦截(不是「找更好」的渠道)。
  • 非购物快捷路径:system prompt 增加品类行情查询(category_insight → chat_fallback)和外部事实查询(web_search → chat_fallback)的短路径,不再强迫所有 query 走完整购物流程。

10. M4 原始边界(已由后续里程碑收口)#

  • 主循环:M9 已组装。
  • 事件上报:M8 已从桩切到真推送;后续又增加了工具 result 摘要字段。
  • category_insight:M5 已从桩升级到真 RAG(2044 张知识库卡片)。
  • 个性化通道:M7/M7.1 已用 only-like 偏好向量驱动。
  • 外部搜索:配好 TAVILY_API_KEY 即走真实 Tavily API,含断路器降级。

面试怎么讲:M4 的产出是「装备齐全的工具箱 + 一条手工能串通的流水线」,不是「会自己干活的 Agent」。把造工具和组装主链路分成两步,是因为工具能单独测、单独验收,合龙时风险更小。后续增强(思考结果摘要、多维 filter、门控放宽)都是在不改变分工的前提下提质——工具签名加参数,不动流水线架构。