本页只译了一部分
本页 44 段里还有 2 段没译成简体中文,没译的段落显示英文原文。逐行搜索
机器翻译译自英文(docs.typesafe.ai)本页 44 段里 2 段还没译
文档索引
在以下地址获取完整的文档索引:https://docs.typesafe.ai/llms.txt 使用此文件可以在进一步探索之前发现所有可用页面。
逐行搜索
为 GitHub 的服务条款构建语义搜索。在一个请求中,通过 Choice 问题针对通俗语言查询对 218 个行 ID 进行评分,并使用 Noul 问题检查文档是否包含答案。
你拥有 GitHub 的服务条款和一个关于它的通俗语言问题。你需要回答问题的行以及检测文档没有答案的方法。包含的查询将具有直接答案的行排在首位。exists 阈值将其余情况分类为缺失或部分。最终得到 find(),它返回 exists 概率和每行的一个相关性分数。
搜索后端由三个部分组成:
- 为每行添加 ID,以便 TypeSafe 可以指向它。
- 使用
Choice问题根据行 ID 回答查询的程度对其进行排名。Choice 问题概率之和始终为 1,因此即使没有行回答查询,某行也会排名第一。 - 在同一请求中,使用
Noul问题检查文档是否包含任何答案。
Setup
获取 TypeSafe API 密钥
在 TypeSafe 控制台创建一个密钥并导出:
export TYPESAFE_API_KEY="your-key-here"安装依赖项
pip install "typesafe-sdk>=0.5.7" cooksafe \
--extra-index-url https://pypi.typesafe.ai/JsonCache 重放包含的 API 响应,因此以下步骤可在无 API 密钥或任何花费的情况下运行。若要改为发送实时请求,设置 TYPESAFE_API_KEY 并删除
json_cache.json。
创建脚本
使用导入和客户端启动 semantic_search.py:
import os
import urllib.request
from pathlib import Path
from cooksafe import JsonCache
from typesafe_sdk import Choice, Noul, NoulCriteria, TypeSafeClient
TYPESAFE_MODEL = "jev-1.12"
client = TypeSafeClient(
api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"), timeout=120.0
)
json_cache = JsonCache(Path("json_cache.json"))步骤 1:为每一行标记 ID
测试文档是 GitHub 的服务条款,拆分为 218 个条款,因此每个搜索结果都指向一行可引用的文本。
添加到 semantic_search.py:
GIST = (
"https://gist.githubusercontent.com/eugene-shvarts/900632789a24983d5678ffd508dd01f6"
"/raw/cf9c2ab422d568deade949ef0a06bed6896964b9/github-tos.txt"
)
@json_cache
def fetch_document(url: str) -> str:
request = urllib.request.Request(
url, headers={"User-Agent": "typesafe-cookbook/1.0"}
)
with urllib.request.urlopen(request) as response:
return response.read().decode()
LINES = fetch_document(GIST).splitlines()缓存防止重复下载,splitlines() 留下由 218 个字符串组成的列表。
现在为每行添加简短 ID 并按行合并回一个文档。模型使用这些 ID 指向其答案。
def line_id(i: int) -> str:
return f"L{i:03d}"
DOCUMENT = "\n".join(f"{line_id(i)}| {line}" for i, line in enumerate(LINES))DOCUMENT 现在如下所示:
L052| You own Your Content. If you post Content you did not create, you are responsible for...
L053| You grant us and other Users the licenses in Sections D.4–D.8. These licenses apply...
L054| 4. License Grant to Us
步骤 2:询问答案所在之处
Choice 问题会为每个选项返回一个概率。将行 ID 作为选项提供,于是“选择一个选项”就变成了“指向某一行”。
def where_question(query: str) -> Choice:
return Choice(
instructions=f'Which line of the document contains the answer to: "{query}"?',
criteria={line_id(i): None for i in range(len(LINES))},
)选项描述为 None,因为文档中已包含每个 ID 对应的文本。查询内容放入 instructions 中;每次搜索之间的状态保持不变。
Info
A Choice question accepts up to 255 options, so this recipe searches documents of
up to 255 lines in one request. Past that, search in two passes: one Choice question picks
a window
of lines, and a second ranks the lines inside it.
步骤 3:检查是否存在答案
Choice 概率之和始终为 1,因此即使文档未回答问题,某行也会排名第一。仅凭排名无法区分真实答案与最接近的不相关行。
因此在同一请求中提出第二个问题:
def exists_question(query: str) -> Noul:
return Noul(
instructions=f'Does any line of the document address or answer: "{query}"?',
criteria=NoulCriteria(
true="At least one line of the document states or directly implies the answer",
false="No line of the document addresses this",
),
)与 Choice 概率不同,Noul 概率不依赖于其他选项,因此当文档没有答案时它可以接近零。
步骤 4:在一次请求中发送两个问题
system_one 方法在一次传递中回答两个问题。状态只发送一次,因此添加存在性检查只需少量额外输出。
@json_cache
def _find(
model: str,
state: str,
where: Choice,
exists: Noul,
) -> dict:
response = client.system_one(
state=state,
questions={"where": where, "exists": exists},
model=model,
)
probabilities = response.answers["where"].probabilities
return {
"exists": response.answers["exists"].noul,
"relevance": [probabilities.get(line_id(i), 0.0) for i in range(len(LINES))],
}
def find(query: str) -> dict:
return _find(
TYPESAFE_MODEL,
DOCUMENT,
where_question(query),
exists_question(query),
)relevance 列表按文档顺序为每一行保留一个分数。
步骤 5:读取结果
两段本地代码完成工作:verdict() 将原始 exists 概率转换为三种状态,其中中间状态用于部分答案,show() 将 relevance 渲染为条形图,以便在终端中可读地显示排名。
FOUND, ABSENT = 0.7, 0.35 # present answers typically read >=0.9, absent <=0.05
def verdict(exists: float) -> str:
if exists >= FOUND:
return "answered in this document"
return "not in this document" if exists < ABSENT else "partially addressed"
def show(query: str, top: int = 4) -> dict:
result = find(query)
print(f'"{query}"')
print(f" exists {result['exists']:.2f} -> {verdict(result['exists'])}")
ranked = sorted(
range(len(LINES)), key=lambda i: result["relevance"][i], reverse=True
)
for i in ranked[:top]:
bar = "#" * max(1, round(result["relevance"][i] * 12))
preview = LINES[i][:58].rstrip()
print(f" {line_id(i)} {result['relevance'][i]:.2f} {bar:<12} {preview}")
return result这些阈值用于区分下面的示例,但在生产环境中使用前,请根据你的文档进行调优。
步骤 6:运行搜索
提出四个问题:两个有直接答案,一个没有答案,一个有部分答案。
print(f"{len(LINES)} lines, {len(DOCUMENT):,} characters\n")
show("who owns the code I upload?")
print()
show("can GitHub kick me off the platform without warning?")
print()
show("do I have to take disputes to arbitration?", top=2)
print()
show("can minors use GitHub with parental permission?", top=2)218 lines, 43,980 characters
"who owns the code I upload?"
exists 0.98 -> answered in this document
L052 0.95 ########### You own Your Content. If you post Content you did not crea
L046 0.02 # Short version: You own content you create, but you allow u
L051 0.02 # 3. Ownership and License Grants
L217 0.01 # Questions about the Terms of Service? Contact us through t
"can GitHub kick me off the platform without warning?"
exists 0.97 -> answered in this document
L168 0.97 ############ GitHub has the right to suspend or terminate your access t
L167 0.03 # 3. GitHub May Terminate
L000 0.00 # Effective date: April 27, 2026 · A. Definitions
L001 0.00 # Short version: We use these basic terms throughout the agr
"do I have to take disputes to arbitration?"
exists 0.14 -> not in this document
L205 0.86 ########## Except to the extent applicable law provides otherwise, th
L168 0.02 # GitHub has the right to suspend or terminate your access t
"can minors use GitHub with parental permission?"
exists 0.46 -> partially addressed
L029 0.90 ########### You must be age 13 or older. While we are thrilled to see
L012 0.07 # “User,” “You,” and “Your” refer to the individual person,
评分的含义
前两个查询返回直接答案和验证所需的源行。
另外两个示例展示了为什么存在性检查很重要:
- 仲裁: 排名给予最接近的行 0.86 的分数,但
exists仅为 0.14。答案不在文档中。 - 家长许可: 年龄规则排名第一,但它没有回答家长许可是否改变规则。结果为 部分解决。
排名告诉你看哪里;exists 分数告诉你结果是否回答了问题。
在你的文档上尝试
在 TypeSafe playground 中打开标记的合同 以针对相同文本编辑问题。若要搜索你自己的文档,替换 fetch_document() 中的 URL;脚本的其余各行都基于 LINES。