第 14 章
第 14 章:逐行语义搜索
第 14 章:逐行语义搜索
本文整理自 Datawhale 开源项目 datawhalechina/jev-cookbook(CC BY-NC-SA 4.0),源文件:main/04_实战指南/05_逐行语义搜索.ipynb。
本 notebook 把中文镜像站中的 Cookbook 改写成可以逐格运行、修改输入并观察结果的最小实验。 逐行找答案并区分命中与空结果。
运行方式与 ../03_架构模式/01_架构模式.ipynb 一致:有有效的 TYPESAFE_API_KEY 时调用真实的
TypeSafe API;没有 Key 或返回 401 时使用内置的离线示例答案。后续代码不区分两种模式,便于先学习
控制流,再切换到真实模型观察概率和置信度。
学习提示:先顺序运行全部单元格,再回到“定义 state”或“定义问题”的单元格修改内容,重新运行后面的单元格。 API Key 只从环境变量读取,不能写进 notebook。
0. 准备
0.1 安装依赖
%pip install -q -U typesafe-sdk0.2 创建客户端
import os
import statistics
import time
from pprint import pprint
from typesafe_sdk import (
Choice,
Score,
Noul,
TypeSafeClient,
TypeSafeAuthenticationError,
)
API_KEY = os.environ.get("TYPESAFE_API_KEY", "")
client = TypeSafeClient(api_key=API_KEY, model="jev-latest") if API_KEY else None
print("客户端已创建:模型=jev-latest,Key=", "已配置" if API_KEY else "未配置(将使用离线示例)")0.3 离线响应与统一调用入口
class _FakeAnswer:
def __init__(self, type_, **values):
self.type = type_
for key, value in values.items():
setattr(self, key, value)
class _FakeResponse:
def __init__(self, answers):
self.answers = answers
self.nouls = {k: v for k, v in answers.items() if v.type == "noul"}
self.choices = {k: v for k, v in answers.items() if v.type == "choice"}
self.scores = {k: v for k, v in answers.items() if v.type == "score"}
self.model = "jev-latest(离线示例)"
self.usage = _FakeAnswer("usage", input_tokens=0, output_tokens=0)
class TS:
offline = False
_warned = False
@classmethod
def call(cls, state, questions, offline_answers):
if client is None:
cls.offline = True
if not cls._warned:
cls._warned = True
print("⚠️ 未设置有效 TYPESAFE_API_KEY,以下输出使用内置离线示例。")
return _FakeResponse(offline_answers)
try:
return client.system_one(state, questions)
except TypeSafeAuthenticationError:
cls.offline = True
if not cls._warned:
cls._warned = True
print("⚠️ 未设置有效 TYPESAFE_API_KEY,以下输出使用内置离线示例。")
return _FakeResponse(offline_answers)
def answer_line(name, answer):
if answer.type == "noul":
return f"{name}: noul={answer.noul:.2f}"
if answer.type == "choice":
return f"{name}: choice={answer.choice} confidence={answer.confidence:.2f}"
return f"{name}: score={answer.score:.2f} confidence={answer.confidence:.2f}"
print("模式:", "离线示例" if TS.offline else "真实 API(首次调用后确定)")0.4 连通性测试
if client is None:
TS.offline = True
print("⚠️ API Key 未设置,后续单元格使用离线示例。")
else:
try:
ping = client.system_one("你好", {"is_greeting": Noul(instructions="这段文字是在打招呼吗?")})
print("✅ API 连通正常,后续单元格会使用真实结果。")
except TypeSafeAuthenticationError:
TS.offline = True
print("⚠️ API Key 无效,后续单元格使用离线示例。")1. 逐行语义搜索
把文档拆成行,先判断每行是否回答查询,再把最高概率的行作为结果。第二个 Noul 用来判断“文档里是否 根本存在答案”,这让空结果和低相关结果可以分开处理。
1.1 定义查询和文档行
QUERY = "如何修改账单邮箱?"
LINES = [
"你可以在个人资料页修改姓名和头像。",
"账单邮箱位于设置 → 通知 → 账单中,修改后会立即生效。",
"发票下载链接会发送到当前账单邮箱。",
"安全邮箱用于接收登录提醒,不等同于账单邮箱。",
]
print("查询和文档已定义:行数=", len(LINES))1.2 逐行判断语义相关性
OFFLINE = [0.08, 0.94, 0.51, 0.18]
matches = []
for line, offline_value in zip(LINES, OFFLINE):
response = TS.call({"query": QUERY, "line": line},
{"matches": Noul(instructions="这一行是否直接回答查询?")},
{"matches": _FakeAnswer("noul", noul=offline_value)})
probability = response.nouls["matches"].noul
matches.append((probability, line))
print(f"{probability:.2f} {line}")1.3 判断是否存在答案并选出最佳行
best_probability, best_line = max(matches)
answer_exists = best_probability >= 0.50
print("文档中存在答案:", answer_exists)
if answer_exists:
print(f"最佳匹配({best_probability:.2f}):{best_line}")
else:
print("没有足够相关的行,转交更广泛的搜索或人工处理。")观察:把“哪一行最相关”和“是否存在答案”拆成两个判断,代码就能区分命中、弱命中和真正的空结果。
知识补充
- 空结果是合法答案:语义搜索必须显式处理"没找到"(全部低于阈值),不要硬返回最不相关的那个——官方在 Use Case Map 里专门强调空结果出口。
- 省 token 技巧:先用代码过滤明显无关的行(长度、关键词、类型),剩下的才逐行问语义,批量场景能省一大半输入。
- 进阶阅读:逐行判"是否命中"与
10_RAG段落分类.ipynb同构;找 top-k 的排序版见04_重排序.ipynb。
小结
这本 notebook 的边界很清楚:TypeSafe 只负责受限、可编程的判断;排序、阈值、分组、重建文本和 函数分派都由 Python 代码完成。修改输入或问题后重新运行,就能观察“模型答案 → 确定性代码”的变化。