# 项目：阅读推荐与解释服务

原文: https://yishto.com/learn/project-labs/lab-book-recommendation/

作者: [Yishto](https://yishto.com/about/)

先建立合成书目与规则推荐，再加入有出处的解释，处理冷启动、偏好权限、无结果和推荐评估。

首次发布: 2026-11-10
内容更新: 2026-11-10

## 从明确的阅读偏好开始

这个项目提供一份能解释推荐理由的阅读清单。练习书目使用合成 ID、标签和简介，不采集真实读者行为，也不把一次点击当作某人的长期兴趣。先让用户选择“AI”“系统”“设计”等类别，返回匹配条目；没有匹配时解释筛选条件，让用户调整偏好。

第一版只需要目录、筛选、排序和反馈。模型可以帮助组织推荐理由，但书籍是否存在、属于哪些类别、有没有合适版本，都由目录记录决定。不要让模型凭记忆补作者、出版时间或用户没看过的书。规则排序能给后续模型方案提供对照，否则很难判断增加调用成本是否换来了实际改善。

把需求写成能验收的行为：同样的书目版本和偏好得到同样的顺序；用户能看到匹配标签；空偏好有明确入口；停用模型后仍能浏览推荐；用户只能读取和修改自己的偏好。推荐解释与推荐排序分开，解释服务失败不应清空已经算好的列表。

## 数据模型决定理由能否追溯

`book` 保存条目 ID、名称、简介、状态与书目版本，`tag` 定义受控类别，关联表记录书籍与类别。偏好记录属于某个用户，并保存修改版本。`recommendation_run` 记录使用的书目版本、偏好快照、排序规则版本、候选 ID 和分数，解释则关联这次运行。

删除或禁用一个条目后，新推荐应排除它；旧推荐记录仍要说明当时使用了什么数据。可以保存必要的条目快照，也可以保留版本化目录，选择取决于存储和留存政策。不能把当前简介直接当作旧推荐当时的证据。

偏好是用户明确提交的数据。服务端从认证上下文确定归属，不接收一个任意 `user_id` 就替别人写入。修改偏好与记录版本放进同一事务；读取推荐结果也检查拥有者。演示中使用虚构数据，实际产品还要提供清除历史、撤回偏好和相应留存说明。

## 做一个可以检查的排序基线

标签集合可以用 Jaccard 相似度：交集大小除以并集大小。书籍与偏好共享的标签越多，分数可能越高；书籍额外包含的标签也会影响分母。它衡量集合重叠，不能证明书籍质量或读者满意度。

下面的三个书目 ID 都是合成数据。两个 AI 条目中，`B-1` 与偏好完全重合，`B-3` 只共享一个标签。排序先看分数，再用 ID 固定并列顺序。

```python
books = {"B-1": {"ai", "systems"}, "B-2": {"design"}, "B-3": {"ai", "data"}}
preferences = {"ai", "systems"}
def score(tags):
    return len(tags & preferences)/len(tags | preferences)
ranked = sorted(books, key=lambda book: (-score(books[book]), book))
assert ranked[0] == "B-1"
assert score(books["B-2"]) == 0
print([(book, round(score(books[book]), 3)) for book in ranked])
```

输出为 `[('B-1', 1.0), ('B-3', 0.333), ('B-2', 0.0)]`。代码把零分条目也排进列表，便于观察；产品接口可以先过滤零分项，并在过滤后为空时返回无结果。不要把零分条目的最后一名解释成“也很适合你”。

示例没有处理两个集合同时为空的除零情况。实现时先定义空偏好策略：让用户选择类别，或提供明确标注的编辑推荐。若允许空标签，应在相似度函数中规定并集为空时的返回值。数据量较大时再考虑预计算或索引，不能凭这几个条目声称系统具有某种吞吐能力。

## 用 Spring 接口连接完整路径

采用 Spring Boot 时，可以从 Spring 官方 REST 指南建立本地服务，再设计这个项目自己的资源。控制器处理输入和响应，推荐服务负责筛选与排序，数据访问层负责目录与偏好。HTTP 映射和 JSON 序列化只是基础，认证、资源授权和业务校验仍需实现。

例如读取目录使用 `GET /books`，更新自己的偏好使用 `PUT /me/preferences`，提交一次推荐请求使用 `POST /recommendations`，查询已保存结果使用带 ID 的读取接口。分页数量、标签数和文本长度在服务端设置上限；未知标签不能悄悄转成任意查询条件。响应中给出运行 ID 和版本，前端才有办法识别旧结果。

按一条纵向路径开发：导入固定合成目录，完成标签选择，返回确定性排序，然后保存运行记录，最后补解释适配器。前端在请求等待、空结果、解释失败和偏好已更新时显示不同状态。用户修改偏好后，旧请求即使晚返回，也不能覆盖新请求的列表。

## 解释只能使用已选条目的证据

给模型的材料限制为已选条目的 ID、简介和匹配标签，输出绑定相应条目。服务端校验返回 ID 是否在候选集合中、引用字段是否存在、理由是否与该次快照一致。模型写出格式正确的理由，仍可能误述字段；必要时直接使用“匹配了 AI 与系统标签”这样的规则解释。

解释请求设置超时和调用预算，拒绝或中断时保留列表并提供规则理由。不要把任意目录文本当作工具指令，也不要把解释模型连接到修改偏好、删除历史或购买入口。若以后加入外部书目检索，先处理资料权限、来源与版本，再讨论更长的解释。

向量检索可以作为候选生成方法，学习排序则需要相应数据和评估。先记录它们解决的是标签表达不足、候选遗漏还是排序问题，避免用一个模型同时掩盖几个不同缺陷。现有示例是规则排序，并没有训练个性化模型。

缓存键包含书目、偏好和规则版本，并限制在正确用户范围内。共享的目录缓存可以复用，带个人偏好的结果不能按一个通用路径混给其他用户。目录更新后失效哪些缓存要能检查；否则接口虽然记录了新版本，实际仍可能返回旧候选。

## 验收相关性、稳定性和权限

建立独立于调参过程的测试偏好，包括单标签、多标签、空集合、未知标签、无匹配和并列分数。逐项检查顺序、空结果与理由归属；再模拟目录停用、偏好更新、重复请求、模型超时和越权读取。保存版本信息，失败时才能重放当时的条件。

相关性、多样性和解释可信度应分别评估。可请审阅者按明确标准判断候选是否符合偏好、是否重复相近条目、理由是否由目录支持。合成偏好测试通过只说明这些规则被正确执行，不能推断真实点击率提高，也不能用调参时看过的样本证明泛化能力。

第一版验收完成后，再比较规则解释与模型解释。在相同候选上记录错误类型、等待时间和调用成本，确认收益能解决读者的实际困难，再决定是否增加模型范围。

## 资料来源与延伸阅读

- [Building a RESTful web service](https://spring.io/guides/gs/rest-service)
- [PostgreSQL transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html)
- [Evaluation best practices](https://developers.openai.com/api/docs/guides/evaluation-best-practices)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [RAG 的最小闭环：检索、证据、回答](https://yishto.com/learn/rag/retrieval-pipeline/)
- [指标、图表和实验怎样回答同一个问题](https://yishto.com/learn/data-practice/metrics-analysis-and-experiments/)
