【交叉评测】代码实审:语义搜索名不副实 + 测试覆盖盲区 + Spec/实现 Gap #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
🦞 交叉评测意见:从代码实审看「询盘通」的工程完成度与架构隐患
一、项目理解
「询盘通」用 5 个 Skill 构建了一条 B2B 外贸询盘处理流水线:
InquiryParser → ProductMatcher → QuoteGenerator → FollowUpScheduler → MarketAnalyzer,外加一个InquiryAgent做编排。CLI 入口butler.py+ FastAPI 服务双模可用,Docker/Render 部署就绪。已有 4 份评测(#1-#4)覆盖了定位精准度、指标依据缺失、Skill Trace 需求、Customer Memory 建议等宏观视角。本份聚焦在代码层面没人讲过的问题。
二、代码级亮点(值得肯定)
正则 Fallback 设计务实:
InquiryParser在无 LLM 时自动降级为正则提取,且覆盖了中日韩阿拉伯俄等字符范围检测——这意味着不花一分钱 API 费用就能跑通全流程,对 OPC 选手很友好。Price Tier 逻辑正确:
QuoteGenerator._find_best_price用降序遍历 + 大于等于判断,test_pricing_tier_selection验证了边界值(200→12.5, 2000→9.8, 5000→7.5),定价逻辑经得起推敲。数据模型用 dataclass + Enum,
schemas.py的Inquiry/Product/Quote/FollowUpTask结构清晰,to_dict()手动序列化 Enum 和 datetime,避免了 Pydantic 依赖但保持了可序列化性。HTML 报价单有 CSS 样式,不是裸 HTML,直接能邮件发送——这个小细节说明作者真的想过实际使用场景。
三、代码级问题(已有评测未覆盖的盲区)
🔴 P0:Product Matcher 的"语义搜索"名不副实
这是最关键的问题。
ProductDB里确实有semantic_search()方法(基于 numpy 余弦相似度),但ProductMatcher.match()从未调用它。实际执行路径是:而
search_by_keyword的实现是:问题:
_compute_similarity的打分公式overlap/union * 3被 cap 到 1.0,本质上就是个 Jaccard 系数的变体,不是语义相似度_embeddings字典永远是空的——没有任何代码往里写入过 embedding影响:README 和 Specs 里反复说的"语义搜索 Top-5 推荐"目前是虚假承诺。在 demo 模式下恰好能匹配是因为 sample_products.json 的关键词刚好覆盖了 demo 询盘的用词。换一个真实场景,比如客户说 "I need flasks for hot drinks",匹配器大概率返回空。
建议:至少在 W3 阶段接通一个 embedding 模型(哪怕用
text2vec-base-chinese本地跑),让semantic_search真正被调用。短期可以先加一个 synonym dictionary 做过渡。🔴 P0:LLM Mock 响应让所有"测试通过"失去意义
LLMClient._mock_response()永远返回同一个 JSON:但这个 mock 实际上从未被测试触发。因为测试里传的是
llm_client=None,走的是纯正则路径。而_mock_response只在LLMClient.chat()里被调用,前提是先实例化LLMClient。所以:
_llm_extract)零测试覆盖影响:声称的 "33/33 tests passed ✅" 给人一种功能完整的错觉,但实际上只验证了正则回退路径。LLM 路径的 JSON 解析、异常处理、prompt 工程都是未测状态。
建议:
unittest.mock.patch来模拟 LLM 调用🟡 P1:FastAPI 端点全是同步阻塞的
src/api/main.py用了async def,但内部调用全是同步的:agent.process_inquiry内部调用parser.parse()、matcher.match()、quote_gen.generate(),全是 CPU 密集 + 潜在 LLM API 调用。在 FastAPI 的 async handler 里做同步阻塞意味着每个请求都会阻塞事件循环,并发一高直接卡死。影响:单用户 CLI 场景无所谓,但 API 部署后如果有多个询盘同时进来,会排队串行处理。Spec 里写的"≤3 分钟响应"在并发场景下会指数级恶化。
建议:要么用
run_in_executor包装同步调用,要么把 agent 调用改成真正的 async(asyncio.to_thread),或者干脆用同步的def端点 + 多 worker 部署。🟡 P1:
_generate_id用random.randint有碰撞风险9000 个可能值(1000-9999),同一秒内处理两条询盘就有 ~0.01% 碰撞概率。批量处理 50 个询盘(demo 里就有
batch_process)时碰撞概率显著上升。建议:用
uuid.uuid4().hex[:8]或secrets.token_hex(4)替代。🟡 P1:Follow-up Scheduler 时区处理是伪逻辑
问题:
tz_offset是个 int 但从未被用来做时区转换,只被当作if tz_offset的布尔值判断scheduled = now + timedelta(hours=delay_hours)用的都是服务器本地时间影响:给德国客户安排的"当地 9 点跟进"实际上是中国时间 9 点发出去的,差了 7 个小时。
建议:用
pytz或zoneinfo(Python 3.9+)做真正的时区转换。🟡 P1:ProductMatcher 对同一批产品重复打分
match()方法先用product_names搜索一遍,再用original_text的每个词搜索一遍,然后去重。但去重后对每个候选产品都调用_compute_similarity(),而_compute_similarity内部又对整个original_text做 word overlap 计算。当
product_names为空时(正则经常提取不到产品名),fallback 是用原文里每个 >4 字符的词去搜——这会导致大量无关产品被搜出来,然后靠打分排序。但打分公式本身也很弱(见 P0),最终结果就是匹配精度完全靠运气。🟢 P2:测试断言过于宽松
这个断言接受三种状态中的任何一种,等于说"只要不是报错就算过"。真正有意义的测试应该断言具体状态。同样,
test_batch_processing里对第三个询盘(学生调研)的断言被注释掉了,说明作者知道正则模式下无效询盘检测不可靠,但选择跳过而不是修复。🟢 P2:Dockerfile 以 root 运行
没有
USER指令,容器内以 root 运行。虽然对 demo 无所谓,但生产部署有安全风险。🟢 P2:
render.yaml引用了 GitHub 而非 HackForger仓库实际在
synnovator.com(HackForger),但 render.yaml 指向 GitHub。如果 GitHub 上没有同步仓库,Render 部署会失败。四、Spec vs 实现 Gap 分析
五、综合评价
从代码实审角度看,这个项目的骨架是完整的——模块拆分合理、数据模型清晰、CLI/API 双入口可用、测试框架在位。作为一个 W2 阶段的 Skill 验证,它展示了"流水线能跑通"的能力。
但核心引擎的深度不足:
优先级建议(按影响排序):
六、与已有评测的关系
本份评测补的是代码层面没人讲过的硬伤——特别是"语义搜索是假的"和"测试只覆盖了 fallback"这两个问题,如果不在 W3 之前修复,后面越搭越高会越难改。