雷达加了第二张脸——这回是给 Agent 看的
上一篇讲的是我用 AX 雷达的一手体验(第一篇)。那时候它只有一张网页脸——我每天打开 news.ax0x.ai、扫首屏、点进看深度解读。
问题是:Claude 看不见这张脸。
我在 Claude Code 里问"今天雷达上有啥值得看",它只能老老实实回"我查不到,你要不截个图"。模型没有我的订阅列表,也没法调出我精选的那 15 条。雷达在它眼里就是不存在的。
这周我给雷达加了第二张脸:一套 HTTP API + 一个 MCP server + 一份 Claude Code Skill。加完之后,我在 Claude Code 里打"brief 我一下这周的雷达",它自己查到 30 条 featured、按信源分组、引用编辑点评。没有胶水代码,没有截图。
核心数据:
- 2 个 commit,约 1600 行代码,改 20 个文件
- 14 个真跑 Postgres 的集成测试(
Request对象直接打路由,不起 HTTP server) - 8 个 REST endpoint + 7 个 MCP tool + 1 个 resource
- 覆盖当前数据:6821 条已加工、59 个信源、30 天 6.8 万次 LLM 调用
一、两张脸,一个后端
这次最关键的一件事:没建两个后端。HTTP API 和 MCP server 共用同一层 SQL 查询。
为什么两个都要做?因为它们解决的是不同的接入成本问题。
HTTP API(/api/v1/*)是兜底路径。OpenAI function calling、Gemini tools、n8n、LangChain——任何会讲 REST 的 Agent runtime 都能用。MCP 还没普及到所有 runtime,HTTP API 是保险选项。
MCP server(/api/mcp)是快车道。Claude Desktop、Cursor、claude CLI 都会自动发现 MCP。我只要在 claude_desktop_config.json 里粘一段 JSON,工具就出现了——不装 SDK,不写胶水代码,不部署 middleware。
如果只做 HTTP API,Claude Desktop 用户每次都得手写一个 function wrapper。如果只做 MCP,n8n 用户就没戏。所以两个都做。
关键是 MCP 那层是薄壳:它不重新写业务逻辑,直接调 API 那一层的 Postgres query。打分策略改了、字段改了、cluster 去重阈值变了——两边同时生效,不会出现一张脸是 v3 另一张是 v2 的情况。
两个薄壳好过一个重接口。
二、sha256 不是 bcrypt,因为 token 不是密码
鉴权用 Bearer token。每个 token 是 32 字节随机值(crypto.randomBytes(32),256 bit 熵)。数据库里只存 sha256 哈希,不存明文。
很多人一看到"存 token"就条件反射想 bcrypt。这个反射在这里错了。
bcrypt 是给低熵密码用的。用户密码常常只有 30 到 40 bit 熵("password123"、"qwerty" 这种),bcrypt 的慢是防撞库的唯一手段。
API token 是 256 bit 熵。宇宙热寂都撞不出来。
换成 sha256 + 唯一索引之后,每次请求的查表是 O(log n):SELECT * FROM api_tokens WHERE token_hash = \$1,一趟 btree 索引命中。换成 bcrypt 就得全表扫 + 逐行比对,QPS 一上去这张表就化了。
sha256 对高熵 token 就像 bcrypt 对低熵密码——不是替代品,解决的是不同问题。
三、语义搜索吃老本:pgvector 早就在了
写 API 之前我本来担心语义搜索这块:要不要单独加个向量库?是不是得跑一遍 Pinecone?HNSW 参数得怎么调?
都不用。M2 那会儿就把 text-embedding-3-large 的 3072 维 embedding 塞进 halfvec(3072) 列 + HNSW 索引了。原本用途是跨源去重聚类——同一事件被 5 家同时报道时合并成一条。
给搜索用就是:把 query 也 embed 一次,然后 ORDER BY embedding <#> \$q。负内积排序跟余弦距离对单位向量等价,能省掉归一化那一步。
总共约 24 行 SQL,加一个 embed 调用。p50 延迟 250 毫秒,基本都花在 embed 调用上(150 毫秒),SQL 本身 80 毫秒。单次查询成本 $0.00002。
这件事教我一个经验:早年装的基建要是设计得通用,后面的新需求往往就是 24 行 SQL 的事。HNSW 索引从 M2 开始一直躺在那儿跑去重聚类,等到要做语义搜索,它已经就位了。
四、MCP 告诉 Agent 工具存在,Skill 告诉它什么时候用
MCP 协议只导出工具签名:名字、参数、返回结构。它不导出语义。
Agent 调完 ax_radar_feed 返回 importance: 72,它根本不知道这个分高不高。HKR 三个轴是什么意思?featured 和 all 怎么选?什么时候用 lexical 搜索、什么时候用语义搜索?这些都是运营决策,不在协议里。
于是我写了一份 Skill 文件放在 ~/.claude/skills/ax-radar/SKILL.md。它的 description 字段调得很具体——"brief me on the radar"、"save this for me"、"search the radar for X"——匹配到这些短语就自动加载进 context。Skill 正文是纯 domain 知识:
- HKR 三个轴分别代表什么、featured / P1 的阈值各是多少
- 什么时候用语义搜索:找"大意"时用;查精确字符串时用 lexical
- 守则:别把整个 feed 灌进对话(污染 context);保存要用户明确要,别猜着存;YouTube 信源永远不会被 excluded(运营决策)
- 三种 MCP 客户端(Claude Desktop / Cursor / claude CLI)的 config 模板
Skill 这层是 Agent 工具设计里最容易被忽略的一环。MCP 是管道,Skill 才是说明书。没说明书的管道,Agent 用起来像拿着扳手挖坑——工具在手里,但不知道往哪使劲。
五、"brief 我一下这周的雷达" 实际走的路
用户侧:我在 Claude Code 里打了这一句。
系统侧发生的事:
- Claude Code 的 skill loader 扫所有 skill 的 description,发现 ax-radar skill 描述里有"brief me on the radar"。加载进 context。
- Skill 内容告诉 Claude:这种"这周概览"问题用
ax_radar_feed工具;简报优先走 markdown resourceax-radar://today而不是 raw JSON;按信源分组;突出has_commentary: true的条目。 - Claude 发
tools/call ax_radar_feed {tier: "featured", limit: 30}给 MCP server。 - MCP server 验 Bearer token(sha256 查表约 5 毫秒),跑
getFeaturedStories查询,返回 30 条 JSON。 - Claude 再发
resources/read ax-radar://today,拿到预格式化的 markdown 简报。 - Claude 用 markdown 做骨架,引用
item_id和editor_analysis字段,输出总结。
全程几秒钟。我写了一句话。一周前这条路径不存在。
六、没做的事
Webhook push 还没做。目前 agent 是 pull——我问它才去查。Webhook(新 P1 文章触发通知)是 v2 的事。
多用户 scoping 没做。当前一个 Bearer token 对应一个雷达实例,所有人看同一份精选策略。以后给团队用得加 org_id 列 + 行级策略。
Agent-to-agent 没做。理论上可以有一条链:一个 agent 从 X / arXiv 抓信号 → 写进雷达;另一个 agent 消费雷达 → 给我写早报。目前只有后半段。
尾
这次 ship 没造什么新东西。API 是包在已有 SQL 上的薄壳,MCP 是包在 API 上的薄壳,Skill 是包在 MCP 上的说明书。加起来的价值不在任何单层,在"同一个后端、多张脸"这个结构。
大多数产品只想着做网页或 App 那一张脸,然后就收工了。但一个产品只要数据值钱,它就迟早有除人以外的消费者——agent、另一个服务、合作方的 pipeline。这些消费者不用网页。他们要的是稳定契约:HTTP、MCP、gRPC、webhook,挑一两个合适的。
所以造产品的下半场不是加功能,是给功能加接入方式。一张脸给人看、一张脸给 agent 看、也许第三张脸给合作方看。后端永远是同一个。