← Together

engineering stories

三个真实战役

比功能更能说明水平的,是遇到没有标准答案的问题时怎么想。 以下三段都发生在这个项目里,过程原样呈现。

系统性排障React Native · Hermes · WebView

战役一:真机地图黑屏,四层洋葱剥到底

iPhone 真机上,创建聚会页的高德地图是一个纯黑框——没有报错、没有搜索框, 而同一份代码在电脑浏览器里一切正常。

第一层 · 现象与假设淘汰

先后排除了高德域名白名单(WebView 内嵌页面来源是 about:blank,白名单必拒——但清空后依旧黑)、 手机代理拦截(脚本实际加载成功)。教训:给黑框装上「错误自述层」,让界面自己说出错原因, 不再靠猜。

第二层 · Hermes 的 [bytecode]

错误自述层报出 Can't find variable: bytecode。 根因:地图代码靠 Function.toString() 把渲染函数注入 WebView,而 Hermes 引擎默认不保留函数源码,注入的是 [bytecode] 占位废码—— 只在真机炸、电脑永不复现。官方逃生门 "show source" 在 Expo Go 下也不生效。

第三层 · 无 WebGL 的「无错全黑」

改为托管页架构后仍黑:网络面板显示零瓦片请求——高德 JSAPI 自动升级到矢量渲染引擎, 在禁 WebGL/无 GPU 环境下「不报错但一个像素也不画」。强制layers: [new AMap.TileLayer()] 走经典栅格瓦片(纯 img,任何设备可渲染)。

第四层 · 一行 CSS 的真凶

还是黑。逐项检查 DOM 发现容器 clientHeight = 0: 高德初始化会把容器重置为 position: relative, 靠 absolute + inset 撑尺寸的容器被塌成 0 高—— 「地图创建成功,但没有一个像素可画」,全设备全版本成立。显式宽高一行修复。

架构决策

最终形态:App 内地图 = WebView 加载自有域名的托管页(/amap/picker、/amap/view), 渲染函数被 Web 与 App 直接 import——全仓库一份地图代码,行为与真实浏览器完全一致, 还天然兼容未来的域名白名单。每层排查都沉淀成页面上的阶段水印与错误自述,下次问题会自己开口。
AI 可信度设计多智能体 · 防幻觉

战役二:让 AI 一个地点都编不出来

聚会方案最怕「AI 推荐了一家不存在的店」。Together 的答案不是祈祷模型听话, 而是让架构使幻觉在结构上不可能出现。

原则 · LLM 只想,代码算数

四个智能体(选址→公平评分→策划→核验)只做权衡与创意;所有数字——每人距离、 通勤耗时、评分、人均消费——一律由代码计算或来自高德真实数据,提示词里明令 AI 不得自报数字。

约束 · 商家闭集

策划智能体只能引用给定商家池里的真实 id;商场/综合体只能点名高德返回的真实内部店铺; 组装层把每个引用对回商家池,对不上的直接丢弃——宁缺毋滥。核验智能体只有「可行/不可行」 判定权、没有改写权,防止它「好心」把真数据改成幻觉。

兜底 · 确定性收口

LLM 输出不合格时,标题由代码从真实站点合成(「去[商圈][活动]吃[品类]」)、 人均距离一律服务端重算(连模型幻觉出的数字都会被覆盖);整份结果过 Zod Schema, 不合格整体作废走降级链,绝不半残上屏。

验证 · 对抗式审查

上线前用多个独立审查智能体互相反驳验证,抓出「兜底路径把演示常量冒充真实距离」等 3 个真实缺陷——审查本身也是多智能体的。
算法与产品的接缝LBS · 公平性

战役三:「公平」怎么变成一个可计算的东西

「找个对大家都公平的地方」是聚会最大的隐形矛盾。把它工程化, 关键在于分清哪一步交给数学、哪一步交给模型。

第一步 · 数学画像

以全员坐标的公平中心为锚搜索真实商家,对每个候选区域计算每位朋友的直线距离 (haversine)——不让 LLM 估距离,也不为每个候选区域都调路线接口 (高德并发限制会被打爆,这是真实踩过的坑)。

第二步 · 模型权衡

把距离画像连同偏好一起喂给评分智能体:它做的是「小张远 2 公里但那里更适合看展」 这类人类式权衡——这是模型擅长、公式不擅长的部分。

第三步 · 真实校准

只对最终选定的区域调用一次高德公交路线,得到每个人的真实通勤耗时; 每套方案上的「距大家人均 X km」徽标,就是这套计算的用户可见面。

取舍记录

直线距离 ≠ 通勤时间(隔江对望的情况会失真)——已列入灰度实验: 「人均 X km」对比「你约 X 分钟」两种呈现,用成局率说话。