从一个工具到两个工具:做一个能解释差异的商品对比助手

用户在商城选中了三款通勤包,问:“哪款更轻、容量更大,现在还有货吗?”

商品参数和库存通常来自不同接口。只查参数,回答不了能不能买;只查库存,又解释不了三款商品的差异。把两个工具都接上以后,新的问题马上出现:其中一件商品查失败,表格还要不要显示?库存没拿到,应该写“缺货”还是“未知”?

这一篇做一个可以操作的商品对比助手:查询两类信息,按用户选中的商品组装对比表,缺什么就明确标出来。 比较对象始终保留,工具异常也能在页面上找到对应位置。

本文的三款商品、库存和模型决策都是固定演示数据。两个工具在浏览器本地运行,没有调用真实模型、商品服务或库存系统,不提供购买与下单功能。页面中的价格和数量不代表真实市场信息。

机器人用参数尺和库存清单比较三只背包,其中一份参数缺失时,仍然保留全部比较对象。

先固定比较对象,再规划调用

本例的比较对象固定为 bag-01、bag-02、bag-03,最多三件。为了聚焦多工具结果的处理,页面没有自然语言解析或开放式商品搜索。

模拟模型提出两类只读调用:get_product_specs 查询名称、价格、重量和容量;get_stock 查询当前演示库存。每件商品分别调用两个工具,因此这一轮是两类工具、最多六次调用。

演示计划的工具名来自应用预定义的两类工具;每个任务在执行前核对商品 ID 与当前比较集合。接入真实模型计划时,还需要在运行时校验工具名允许列表。模型如果把第三件商品改成一个未知 ID,不能把那个 ID 直接查下去,也不能擅自替换成另一件商品。页面仍保留用户请求的第三行,并说明它的数据为什么没有取得。

实际接入时,比较对象应来自用户明确选择的商品集合。模型可以帮助规划查询,但不能在整理答案时悄悄删减比较对象;否则一张看似完整的表,很可能根本没有回答原来的问题。

把“成功”和“未知”放进数据结构

直接给库存字段一个默认值 0 很方便,但会丢失关键信息。库存零表示查询成功且当前数量为零;未知表示查询失败、返回不合法,或对应调用根本没有获准执行。它们需要不同的界面文案和后续动作。

可以把每份工具结果表示成下面这种结构:

typescript
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

每行各自保留一份规格结果和一份库存结果。规格失败但库存成功时,库存仍然可以显示;库存失败但规格成功时,已经拿到的重量、容量也不应消失。页面不把“有一部分数据”直接包装成“全部查询成功”。

规格中的价格用整数分,重量用克,容量用升。库存数量必须是非负整数。外部数据进入页面前需要检查字段类型、数值范围和允许长度,不能仅靠 TypeScript 声明相信返回值。

等每个任务给出结果,单独处理失败

这六个查询相互独立,适合并发执行。示例使用 Promise.allSettled() 收集每个任务的结果:成功任务得到 fulfilled 分支,失败任务得到 rejected 分支,随后分别转成行内状态。

它返回的结果顺序与输入任务顺序一致,但应用仍然需要保存每个任务对应的商品 ID 和工具名,才能把失败原因放回正确的单元格。不要把成功结果先过滤出来,再按数组位置与三件商品拼接。

allSettled() 的作用是收集独立任务的完成状态,不会替工具校验数据,也不会为一直不结束的请求自动设置超时。六个固定本地任务有明确数量上限;真实接口仍需单独设计超时、取消和并发限制。MDN Promise.allSettled 说明

执行轨迹记录的是本地工具实际成功、失败或被拦截的结果。这里没有模型服务调用,也没有 token 流式输出;不能把轨迹中的模拟计划当作真实模型遵循工具协议的证据。

按请求列表组装三行表格

正常场景中的演示数据如下:

商品 ID 名称 重量 容量 库存
bag-01 轻量通勤包 520 克 18 升 12
bag-02 大容量通勤包 780 克 24 升 5
bag-03 紧凑通勤包 450 克 15 升 8

表格行由请求列表生成,查询结果只是给这些行补充字段。spec-failure 场景让第二件商品的规格查询失败,第二行继续显示,规格区域标为未知并提供原因。这样用户能区分“少了一件商品”和“这件商品暂时没有足够信息”。

本例在 Vue 渲染时使用商品 ID 作为稳定的行 key。Vue 文档建议为需要保持身份的列表项提供唯一 key;它帮助组件维护对应关系,但不能代替结果组装时的 ID 校验。Vue 列表渲染说明

如果参数缺失,就不对该商品作重量或容量排名。如果库存未知,就不推断它缺货,也不推断它有货。真实场景中,哪怕库存查询成功,也只是一次查询结果;最终能否下单仍由交易服务确认。

解释只能复述有依据的差异

正常数据可以支持这样的描述:“第三款演示商品重量为 450 克、容量为 15 升;第二款容量为 24 升、重量为 780 克。”这些都是查询字段,而不是额外推测。

它们还不能支持“背着更舒服”“更耐用”“适合暴雨”等结论,因为本例没有相应证据。固定模拟理由仅复述已取得的重量、容量和库存状态;缺失时说明信息不全,不补造性能评价。

汇总文案采用确定的完整与缺失统计,不让一段流畅的解释覆盖表格中的异常。真实模型如果负责生成解释,还要约束它能引用哪些字段,并验证它是否提到了不存在的商品或没有查询到的属性。JSON 结构正确并不意味着语义正确。

页面把理由作为 Vue 文本节点展示。HTML 文本场景用来检查这些字符会不会被解释成标签;本例不接收模型生成的页面模板或购买链接,也不会执行模型输出的代码。

部分失败时,页面应该怎样回应

演示提供六个可切换场景,观察同一份表格在不同数据质量下的变化:

场景 应看到的结果
正常查询 三行商品,规格与库存齐全
单件规格失败 第二行保留,规格未知,库存仍显示
单件库存失败 第三行保留,库存未知,规格仍显示
库存确实为零 第二行明确显示缺货,不能显示为查询失败
模型给未知商品 ID 拦截对应调用,保留原请求行并标出缺失
理由含 HTML 字符 按文本显示,不创建标签或触发事件

再次运行或重置场景时,页面应清掉旧的行和说明,避免把上一次成功结果混入本次比较。这里的查询只读,用户可以再次运行;如果以后增加加入购物车或下单操作,就要另外定义确认、幂等和结果核对流程。

移动端保留表头与对应关系,把横向滚动限定在表格容器内,不能让整张页面横着溢出。示例给表格添加说明,并用 th 与 scope 标记表头。W3C 的表格指南说明了如何建立表头与数据关系;本例只检查相关结构与布局,没有声称完成全套无障碍验收。W3C 表格表头指南

运行与验证这个最小示例

打开商品对比交互演示 · 下载完整源码与离线页面

下载解压后直接打开 index.html,Vue 已打包到本地文件,不需要 API 密钥或 CDN。也可以启动静态服务:

Bash
python3 -m http.server 8000

访问 http://localhost:8000。若要修改 TypeScript 源码,按 README 安装锁定依赖,然后运行:

Bash
npm run typecheck
npm test
npm run build

本次类型检查和 9 个核心测试通过,浏览器实际操作了六个场景:三行商品始终保留,未知与零库存分开,未知 ID 任务被拦截,HTML 字符没有生成标签或触发弹窗。1440 像素与 390 像素视口均没有整页横向溢出,手机表格可局部滚动;演示页未发出外部请求。实际结果记录在源码包的验证说明中。

这些验证覆盖固定模拟流程。它们没有验证真实模型决策、真实库存一致性、账号权限或生产效果;接入外部依赖后应对同一份实现重新验证,不能沿用离线通过的结论。

从两个工具扩展到业务流程

先保留请求集合和每个字段的来源,再添加新的工具,例如配送范围或预计送达时间。每增加一类结果,都要定义成功、未知和不适用分别表示什么,以及哪些结论必须等它查询成功才能显示。

真实服务还会出现版本不一致:参数刚更新,库存却来自另一个时间点。可以在协议中携带数据版本与查询时间,让页面和解释知道依据的新旧;本例没有实现跨服务快照或库存锁定。

两类工具接好之后,前端的主要工作是让信息完整度可见:用户选了哪几件,哪些字段有依据,哪些查询失败,以及当前能够支持什么比较。把这些关系固定下来,才有一张值得继续扩展的 Agent 对比页面。

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信