OpenAI SDK 文件与向量存储

QuibblerAgentQuibblerAgent 2026-11-08 约 20 分钟 12 次阅读

第 6 篇解决了"一份文档怎么喂给模型":上传拿 file_id,input_file 块引用,一份 PDF 经得起连番追问。可文档一多,这条路立刻见顶——几十份手册、几百页笔记,总不能每问一句就把全部原文塞进上下文,窗口装不下,token 账单先撑爆。业界的标准解法是 RAG(Retrieval-Augmented Generation,检索增强生成):提问先检索相关片段,只把片段交给模型。OpenAI 把这条流水线做成了托管服务:Files API 管上传,Vector Stores 管归集与索引,file_search 工具管检索,应用侧一行检索代码都不用写。

本篇是《OpenAI Python SDK 从入门到实战》系列第 10 篇,继续工程落地阶段:把第 6 篇的单文件用法展开成完整的文件体系,把 RAG 从"自己搭"换成"开箱用"。文中 Vector Stores 用法与计费口径核实自官方文档及社区讨论(2026 年 10 月)。全文按"三部曲全景 → 存储管理 → 工具声明 → 结果与引用 → 与自建 RAG 分界 → 计费账 → 个人文档问答实战 → 总结"组织。

1、文件到答案的三部曲

先摆全景。托管检索把"文档进、答案出"拆成三步:上传 → 归集 → 检索。上传沿用第 6 篇的 Files API,purpose 仍是 user_data,一步没变;归集是新增的一层——把一批 file_id 装进 Vector Store(向量存储),服务端自动完成解析、切分、向量化与索引;检索则是 Responses API 的内置工具 file_search,按用户问题查片段、只把命中内容注入上下文。全链路如下图:

flowchart LR A[本地 PDF] -->|上传换 id| B[Files 文件区] B -->|批量挂载| C[向量存储] C -->|切分与索引| D[(片段索引)] E[用户提问] --> F[Responses 请求] F -->|file_search 检索| D D -->|命中片段注入| G[模型作答] G --> H[带引用的答案]

解读:与第 6 篇单文件路线的分岔点在"归集"这一步——单文件用法把整份 PDF 当一块素材直接放进 content,文档一长 token 立刻失控;进了向量存储的文档被切成小片段另建索引,问题到来时只有最相关的几段进入模型,几百页的库问一句也只花两三千 token。三步各有各的入口:上传 client.files、归集 client.vector_stores、检索只消在 responses.create 的 tools 里声明一行——下文逐个展开。

2、向量存储的创建与管理

2.1、创建存储并批挂文件

Vector Store 在 SDK 里是独立资源,入口 client.vector_stores。先创建一个空库:

from openai import OpenAI

client = OpenAI()

store = client.vector_stores.create(
    name="说明书库",
    expires_after={                       # 活跃 30 天后自动清理
        "anchor": "last_active_at", "days": 30,
    },
)
print(store.id)      # vs_ 开头,后续引用全靠它

解读:name 只是给人读的标签,可以重名,程序里认的是 vs_ 开头的 id;expires_after 是值得养成习惯的参数——以最后活跃时间为锚,30 天没人用就自动清理整个库,第 6 节的计费账会解释为什么推荐它。库建好是空的,接下来把文件批量挂进去:

import glob

file_ids = [
    client.files.create(
        file=open(p, "rb"),
        purpose="user_data",              # 与第 6 篇单文件口径一致
    ).id
    for p in glob.glob("docs/*.pdf")
]

batch = client.vector_stores.file_batches.create_and_poll(
    vector_store_id=store.id,
    file_ids=file_ids,                    # 单批至多 500 个文件
)
print(batch.file_counts)                  # 各状态文件计数

解读:上传这一步与第 6 篇完全同源,purpose 仍是 user_data;变化在挂载——file_batches 把一批文件一次关联到库,create_and_poll 是 SDK 的便捷方法,内部"提交批次 + 轮询到终态",返回时解析基本完成。batch.file_counts 里有 total、in_progress、completed、failed 四个计数,failed 大于零通常是个别文件损坏或格式不支持,挑出来单独处理即可。规模口径:单批至多 500 个文件,更多就再提一批;单个库至多 1 万个文件,单个文件 512 MB、500 万 token 封顶——与第 6 篇 Files API 的上限口径一致。

2.2、解析状态与轮询等待

挂载不是瞬间完成的:服务端要解析、切分、向量化,几百页的 PDF 要等一阵。不想用便捷方法、或要在等待间隙干别的活,可以手动挂载并轮询:

import time

vs_file = client.vector_stores.files.create(
    vector_store_id=store.id,
    file_id=file_ids[0],                  # 单个文件也能挂
)
while vs_file.status not in ("completed", "failed"):
    time.sleep(2)
    vs_file = client.vector_stores.files.retrieve(
        vector_store_id=store.id, file_id=vs_file.id,
    )
print(vs_file.status)                     # completed 或 failed

解读:单个文件挂载走 vector_stores.files.create,返回对象自带 status 字段,从 in_progress 走向 completed 或 failed,retrieve 负责刷新;库自身也有同名的 status,全部文件解析完才变 completed——create_and_poll 帮你轮的就是它。工程上建议把建库放进离线任务(脚本启动或定时同步),问答链路只管用现成的 store.id,别让用户等解析。

3、声明 file_search 工具

库就绪后,检索的接入简单到不像话——在 responses.create 的 tools 里声明一行。第 5 篇自定义工具的形制还记得的话,对比着看更清楚:

resp = client.responses.create(
    model="gpt-6.1-terra",
    instructions="只依据文档库回答,检索不到就直说不知道。",
    input="医保报销需要准备哪些材料?",
    tools=[{
        "type": "file_search",            # 内置工具,服务端执行
        "vector_store_ids": [store.id],
        "max_num_results": 5,             # 每次取片段数,默认 5
    }],
)
print(resp.output_text)

解读三个要点。其一,type 是 file_search 而非 function,没有 name、description、parameters——它不是"模型发指令、你来执行"的外挂工具,而是服务端托管的内置工具:模型决定检索时发一条 file_search_call 条目,OpenAI 自己执行检索、自己把片段拼进上下文,没有 function_call_output 回传这一环,第 5 篇的调用循环整个省掉。其二,要不要检索由模型自己判断:寒暄它直接回,问库里的事才发检索——instructions 里那句"检索不到就直说"因此很重要,能把"库里没有"与"现场编一个"分开。其三,max_num_results 控制每次取几段,默认 5,追求省钱省延时可调小;配 ranking_options 的 score_threshold 还能按相关度过滤弱命中。另外注意:file_search 是 Responses 线的内置工具,Chat Completions 那条线没有它,第 1 篇的选型结论在这里又多了一条注脚。

4、检索结果如何进入上下文

片段怎么进模型、引用怎么带出来,是这套机制最值得看透的部分。模型作答前,服务端把命中片段拼进本次请求的输入侧——它们计入 input token、正常计费,但不出现在你写的 input 参数里;自动切分的默认口径约每段 1500 字符、相邻段重叠 300 字符,一段折合几百 token,默认 5 段就是两三千 token 的量级。想把命中片段拿到手上核对,用 include 参数:

resp = client.responses.create(
    model="gpt-6.1-terra",
    input="文档对备份频率有什么要求?",
    tools=[{"type": "file_search", "vector_store_ids": [store.id]}],
    include=["file_search_call.results"],   # 要求带回命中片段
)

for item in resp.output:
    if item.type == "file_search_call":
        print("检索语句:", item.queries)     # 模型改写后的查询
        for hit in item.results:            # 每条含来源与片段内容
            print("命中文件:", hit.file_id)

解读:include 加上 file_search_call.results 后,output 里的 file_search_call 条目携带完整命中——queries 是模型改写后的检索语句,未必与原问题逐字相同;results 逐条给出来源文件与片段内容,排查"为什么没检到"就靠它。答案文本侧还有一层引用:正文中被标注的位置挂着 file_citation 类型的 annotation,含 file_id 与命中文本,前端据此渲染"依据《使用手册》第几节"式的角标即可。两个位置各管一段:results 给程序对账,annotation 给用户看溯源。

5、托管检索与自建 RAG 的分界

file_search 这么省事,为什么业界仍有大量自建 RAG?因为托管换来的是控制权的让渡。两条路线的分界对比如下:

维度 托管检索 自建 RAG
上手成本 十几行代码当天跑通 切分、嵌入、向量库、重排全套自备
检索控制 自动切分与重排,可调参数有限 块大小、混合检索、重排全可定制
数据边界 语料与索引存于 OpenAI 侧 语料不出自己的基础设施
费用结构 存储按天加检索按次加 token 一次嵌入费加向量库托管费
规模上限 单库 1 万文件、单文件 512 MB 无硬上限,看工程能力

量级是第一判据:个人知识库、产品手册、团队文档,几个 GB 以内,托管路线的工程性价比碾压;上了规模或上了合规要求,自建的基础设施才回本。还有个常见的折中姿势——embedding 仍调 OpenAI,向量库与检索自持,保住数据边界的同时少写一半解析代码。

文档进了向量存储,语料与索引就都落在 OpenAI 侧;涉及客户隐私或内部机密的语料,先过合规评估再上传。

6、向量存储的计费账

托管的账单分三段,各段口径如下(现价以官方定价页为准):

计费项 口径 量级参考
向量存储 $0.10 每 GB 每天,首 1 GB 免费 3 GB 存一个月约 $6
检索调用 $2.50 每 1000 次 折合单次 $0.0025
模型 token 片段与生成照常计费 每问约两三千输入 token

代一笔总账:3 GB 的文档库存一个月,超出免费额度的 2 GB 存储费是 2 × $0.10 × 30 即 $6;每天问 100 个问题、每问触发一次检索,一个月 3000 次是 $7.50;token 侧按第 8 篇的单价,Terra 档一个月几美元。三段合计每月十几美元,换来零运维的知识库——这是量级感受,真账以 response.usage 与平台账单为准。

注意:存储费按天收、与有没有人提问无关。用完的库要显式删除,或建库时配好 expires_after 自动过期——忘删的库会安静地每天扣钱。

7、个人文档问答实战

把三部曲组装成一个能用的知识库助手:docs/ 文件夹里放几份 PDF——说明书、保单、租房合同——两段代码跑通。第一段建库,收成可复用的函数:

import glob
from openai import OpenAI

client = OpenAI()

def build_store(name, folder):
    """上传文件夹内全部 PDF 并建成向量存储"""
    ids = [client.files.create(file=open(p, "rb"),
                               purpose="user_data").id
           for p in glob.glob(f"{folder}/*.pdf")]
    store = client.vector_stores.create(name=name)
    client.vector_stores.file_batches.create_and_poll(
        vector_store_id=store.id, file_ids=ids)
    return store

store = build_store("个人文档", "docs")
print(store.file_counts.completed, "份文件就绪")

解读:build_store 把上传、建库、批挂载、等就绪收拢在一次调用里,几份 PDF 几秒到几十秒解析完;打印 file_counts.completed 确认全部就绪再放行。第二段是多轮问答,第 7 篇的 previous_response_id 直接派上用场:

def ask(question, prev_id=None):
    resp = client.responses.create(
        model="gpt-6.1-terra",
        instructions="你是文档助理,回答要注明依据哪份文件。",
        input=question,
        tools=[{"type": "file_search",
                "vector_store_ids": [store.id]}],
        previous_response_id=prev_id,     # 续接上一轮对话
    )
    return resp.output_text, resp.id

answer, sid = ask("保修期是多久?")
print(answer, "\n")
answer, sid = ask("那清洗滤网怎么操作?", sid)
print(answer)

输出类似:

依据《保修条款.pdf》:整机保修一年,电机保修三年。

依据《使用手册.pdf》:断电后取出滤网,清水冲洗晾干再装回。

解读四个要点。其一,store.id 全程只出现在 tools 声明里,问题本身怎么提都不用管文件——检索是模型的事。其二,previous_response_id 把上一轮问答连同检索条目一并续上,追问"那清洗滤网呢"里的"那"字,模型靠链上历史才读得懂。其三,多轮里每一轮都可能触发新的 file_search_call,问题换了主题就换一批片段——自建 RAG 里"多轮改写查询"的活,托管版白送。其四,用完删库一条命令 client.vector_stores.delete(store.id),第 6 节的账别忘了结。

8、总结

本篇把文件体系补完整:上传 client.files、归集 client.vector_stores、检索 tools 里的 file_search,三部曲各管一段;内置工具由服务端执行,没有回传循环,检索与否模型自断;命中片段注入输入侧计 token,include 与 annotation 一个给程序对账、一个给用户溯源;计费存储按天、检索按次、token 照常,首 1 GB 免费适合起步。选型建议:文档几个 GB、团队内部用,托管检索当天上线;语料敏感或规模上去,再认真评估自建。对于囤了一堆 PDF 的开发者,建议先把第 7 节两段代码跑通,把"翻文档找条款"换成"直接问一句"。细节口径见官方文件检索指南与 Vector Stores API 参考,SDK 辅助方法的实现可对照官方仓库。

留一道练习:给第 7 节的 ask 函数加引用展示——遍历 output 里 message 条目的 annotation,把每处 file_citation 对应的文件名拼在答案末尾;再故意问一个库里没有的内容,打印 file_search_call 的 queries 看模型怎么改写检索语句,并检验 instructions 里"直说不知道"的约束是否生效。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

101