我想做一个面向个人阅读清单的音频助手:收藏一篇文章,补一句自己关心的问题,之后能在走路或通勤时听到一段中文讲解。它应该讲清楚一个问题,保留回到原文的入口,也能记住用户想继续追问什么。
这篇文章记录的是方案收敛和实现拆解阶段。我研究了几类公开项目,把内容流程、数据模型和失败边界整理成下面这条实现路径;代码是缩减示意,故障场景是待执行的验证用例。文中的次数、时长和样本量都是初始规格,没有把它们当成已经跑出的效果数据。
先形成可定位的证据,再生成讲稿;先安排收听预算,再消耗生成资源;生成过程允许重试,已发布内容保持固定。
1. 一期节目先回答一个问题
“把文章转成音频”很容易产生一个过大的承诺:既要概括全文,又要补背景,还要解释所有代码和取舍,最后全部压进五分钟。我把单集目标收窄为一个用户真正关心的问题。
例如,用户保存了一篇讨论 Agent 重试的文章,备注是“怎样避免个人助手把同一封邮件发两遍”。这一期就围绕业务幂等、远端结果未知和重试边界展开。文章里的框架历史、完整 API 清单和其他应用案例,可以继续留在原文。音频的任务是建立一个足够准确的理解入口,帮助用户判断下一步该读哪里。
**五分钟在这里是成品上限。**常规目标先设为三到四分半,简单问题可以更短,复杂问题允许提出续篇。系统不能为了填满时长加入重复铺垫,也不能为了压缩时长删掉关键条件。
同样,每天十分钟表示当天安排的收听总量。用户昨天没有听完的节目,今天优先复用并占用预算;资料不足时可以不生成。主动提交的文章、文本和文字型 PDF 是主线,RSS 更新只是候选,不因为某个订阅源更新频繁就挤掉用户主动保存的材料。
这个选择直接影响了架构:选题需要保存用户意图;历史记录需要区分生成和收听;预算要进入数据库;发布之前必须知道真实音频时长。产品范围越明确,后端越容易写出可以验证的规则。
2. 给来源和陈述建立不同的身份
一条 URL 只能告诉我去哪里获取材料。它无法说明某段音频依据的是网页更新前还是更新后的内容,也无法表达用户为什么保存它。
我把投递项、内容版本和讲稿陈述分开保存。sources 保留提交者、输入类型、原始地址、备注和优先级;source_versions 保存正文快照、抓取时间、内容哈希与提取器版本;chunks 记录段落、章节、PDF 页码和标准化文本中的字符跨度。
同一 URL 的两次投递可能对应两个问题,因此不会建立 owner_id + URL 的唯一约束。网络重发通过请求幂等键处理;正文重复则在内容版本层复用。这两个“重复”的含义不同,不能共用一个去重规则。
抽取质量也要保留下来。pypdf 的文本提取说明解释了 PDF 文字顺序、布局和扫描件带来的限制。首版只接收可提取文字的 PDF,正文缺失时展示 PARTIAL 或待补资料,而不是找一篇相似文章继续生成。quality 与 index_state 分开:全文已经提取,不意味着分块与 Embedding 已经完成。
接下来才是讲稿里的 claim。下面只是字段示意,真实引用必须来自已保存的片段:
{
"claim_id": "c03",
"text": "调用超时不能证明远端操作没有完成",
"kind": "external_fact",
"critical": true,
"evidence": [{
"source_version_id": "sv1",
"chunk_id": "ck7",
"span_start": 18,
"span_end": 72
}]
}
程序检查片段是否存在、是否属于允许来源、跨度是否能定位到真实文字;语义核查再判断片段是否支持这句话。文末有来源链接,只完成了很小的一部分工作。真正要防的是“来源说可能,讲稿写必然”“来源针对旧版本,讲稿省掉了版本条件”。教学例子和推断单独标识,不伪装成原作者明确给出的事实。
首版把完整 EvidencePack 和 ScriptDoc 存成不可变 JSONB 产物,不急着拆成一套复杂的 claim 分析平台。这样仍然能从一个讲稿版本回到确切证据;代价是 JSONB 内的引用需要在写入时逐个校验,数据库不会自动替它建立外键。
3. 检索先决定范围,再比较相关性
对于能放进资料预算的短文,我会直接提供带段落编号的全文。长文先从解析出的标题、章节和页码建立目录,再加入摘要、引言、结论的有限样本,帮助系统确定核心问题。需要的细节再通过 scoped RAG 查找。提纲本身也消耗上下文预算,不能先把一百页全文发给模型,再声称后续检索很节省。
检索前先限制 owner_id、允许的 source_version_id 和 Embedding 配置。用户的原文、为当前缺口找到的补充资料、历史节目分别查询。历史节目能提示“这个概念可能讲过”,不能自动成为事实依据,否则系统会不断引用自己之前生成的内容。
首版选择 PostgreSQL 加 pgvector,让权限、内容版本、正文与向量共处一个数据层。关键词通道使用 jieba 预分词、PostgreSQL simple 配置的 tsvector 和 ts_rank_cd;向量通道使用余弦距离的精确检索。**这套 PostgreSQL 全文检索不叫 BM25。**具体分词和排名行为应按全文检索文档解释。
两路初始各取十二条,用 RRF 融合后保留八条,再补足父段落和必要的定义,最后控制在约六千 token 的证据预算内:
# 缩减示意:每一路的 rank 从 1 开始。
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, chunk_id in enumerate(ranking, start=1):
scores[chunk_id] = scores.get(chunk_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
关键词容易抓住“幂等键”和 API 名,向量可以补充“同一封邮件发两遍”这样的改述。RRF 合并的是名次,避免直接相加不同量纲的原始分数。长问题还要先提取有限术语,防止全文查询把所有词项都当作必须同时出现的条件。
我先保留精确向量检索作为基线,再测是否需要 HNSW 或重排。pgvector 的过滤说明提醒了近似索引下过滤可能导致候选不足的问题。当前阶段更需要知道必要证据为什么没进前八条,而不是先增加一层索引,再把召回变化混进内容质量问题里。
4. 用一个有上限的内容图管理判断
首版使用 LangGraph,是因为内容流程存在几个真正需要判断的分支:有没有必要补资料,补充是否足够,讲稿哪里不受支持,能否通过一次修订解决。这些分支值得显式表示。
以上图省略了参数校验等普通程序步骤。核心证据不足时直接停止;可以删除的非核心补充则删去或限定表达。系统没有义务把每个问题都强行回答出来。
扩搜前,gap_plan 必须说明缺什么、为什么影响本期解释、期望找到什么证据。例如,“原文只说失败重试,但没交代请求超时后远端可能已经成功”,这才构成一个明确缺口。搜索“更多 Agent 最佳实践”太宽,无法定义何时结束。
初始限制是每期最多两个缺口、两次搜索、四个新增正文页面、一次事实修订。Tavily 返回的标题和摘要用于筛选入口;进入证据包之前,仍要读取原网页。来源已经足够时,搜索次数就是零。GPT Researcher 的研究和来源接口提供了有用的分层参考,但本项目不默认展开完整的深度研究。
预算由程序执行。每次调用前在 PG 中原子占用额度,调用失败也计数;token 先预留,再按供应商 usage 结算。计数保存在 generation_runs.budget_usage,跨 attempt 累计。换了执行者,就获得一套新的搜索额度,是恢复设计里很容易漏掉的漏洞。
讲稿采用 JSON Object 输出,加 Pydantic 与业务校验。生成和核源可以先用同一个模型适配器,但不能因为同一模型给自己打了高分就宣布准确。固定反例与人工抽样仍然需要保留。
5. 让数据库、队列和图各自回答一个问题
整个系统先做成一个模块化应用,同一份后端代码运行 API、worker 和调度进程。前端用 React 与 TypeScript,后端用 FastAPI;PG 保存状态,Redis/Celery 处理队列,音频和原始文件放在 S3 兼容对象存储。模型通过云 API 调用,不部署本地模型。
这几层保存的信息有交集,职责却需要分开:
| 组件或记录 | 它回答的问题 |
|---|---|
generation_runs | 用户的任务是否被接收,谁在执行,下一步还能否重试 |
| LangGraph checkpoint | 某次内容图执行到了哪里,已有怎样的图状态 |
stage_artifacts | 哪份证据、讲稿或音频已经完整提交,可以复用 |
episode_revisions | 这一版节目选择了哪组证据、讲稿和音频 |
episodes 发布指针 | 用户实际拿到的是哪个固定版本 |
LangGraph 的 checkpoint按照 thread 保存图状态,并支持步骤边界恢复;它不会把模型 API、对象存储和业务数据库合成一个共同事务。因此,“图恢复成功”和“外部副作用没有重复”必须分别讨论。
这一分层也让失败更好解释:讲稿就绪但 TTS 失败,可以保留讲稿;音频就绪但今天安排不下,可以延期;发布已经完成但浏览器断线,可以重新读取状态。它们不需要全部退回“生成失败”,再从第一步重来。
6. 把任务账本放进提交事务
API 收到请求时,最先要建立的是“这件事已经被接受”的事实。假如先写业务表,再向 Redis 发任务,进程可能在两者之间退出;先发任务再写业务表,又可能让 worker 读到不存在的对象。把顺序交换一下,不能消除这个间隙。
我的实现路径借用 Transactional Outbox 的关键思路:**把待处理意图与业务记录一起提交,再由独立执行者投递。**首版没有再加一个通用 outbox 平台,generation_runs 直接兼任持久待执行账本。这个选择适用于当前按 run 唤醒任务的需求;以后需要向多个下游分发完整领域事件,再考虑独立 outbox。
创建节目时,同一个 PG 事务里保存 episode、revision、主 run 和初始事件,并尝试预留今天的收听预算。预留成功才写 QUEUED;不够则保存 DEFERRED,照常返回资源 ID。**DEFERRED 任务不入队、不调用模型。**这样用户的需求没有丢失,系统也不会先花钱生成一堆今天听不完的音频。
dispatcher 只扫描到期的 QUEUED 和 RETRY_WAIT。它用短事务领取投递租约,提交后发送 run_id;没有启动回执时允许补投。发送期间不持有数据库锁。发送成功但回写失败,可能再发一次,这需要 worker 领取执行权时处理,不能靠 Celery task ID 当业务锁。
两个唯一约束尤其值得提前写下:
CREATE UNIQUE INDEX uq_run_request
ON generation_runs(owner_id, kind, request_key);
CREATE UNIQUE INDEX uq_generate_revision
ON generation_runs(revision_id)
WHERE kind = 'GENERATE';
第一条处理 HTTP 重发,同键不同 payload 要返回冲突;第二条保证一个节目版本只有一个主生成任务。次日重排继续使用同一个 run,不会因为新的 plan ID 创建新任务、清零调用预算。明确改问题或生成配置,才创建新的 revision。
Celery 先用 prefork 并发二。late ACK、worker 子进程丢失和 Redis visibility timeout 都影响重投,但没有任何单项配置能代替业务幂等。Celery Tasks明确区分了 late ACK 与 worker-lost;Redis broker 文档也说明了过长 ETA 的重复投递风险。因此远期日程保存在 PG 的 next_due_at,到期才入队;业务重试统一由数据库安排。
7. 接管任务时,旧执行者仍可能回来
lease 过期只能表示系统决定允许别人接管。旧 worker 可能只是网络慢,远端请求仍在运行。它晚到的结果如果继续更新活动讲稿,就可能覆盖新 worker 已经选定的版本。
我把每次领取称为一个 attempt。worker 原子领取 active_attempt_id 和 lease;每约十五秒续约一次,初始租期六十秒。下面是需要通过故障注入验证的场景:
这里的保护来自提交时的检查。涉及节目时先锁 episode,再锁 run,在同一个短事务内核验 attempt 和未过期 lease,选定成功产物、修改 revision 指针、推进 run、追加事件。把“检查执行权”和“写活动结果”拆成两个事务,会重新留下接管竞态。涉及计划的路径统一按 plan → episode → run 加锁。
checkpoint 还有一层问题:业务层的 lease 不会自动约束框架自带的 PostgresSaver。我将 thread_id 设计为 run_id:attempt_id。同一 attempt 可以使用自己的 checkpoint;跨 attempt 接管则建立新 thread,从已经提交的阶段产物继续。旧执行者即使留下 checkpoint,也只落在自己的 thread 中,不能污染新执行者的图。
阶段产物的内容保持不可变,活动指针才允许在合法执行者手里推进。这样做需要显式记录输入指纹和阶段完成情况,工作量比单纯调用一次 graph 大一些;换来的是能清楚判断哪些结果已经属于业务事实,哪些只是某个执行者尚未提交的计算。
实现时还要保护心跳本身。同步 PDF 解析、阻塞 SDK 调用和 FFmpeg 不能长期占用负责续约的事件循环。每个 Celery task 的异步资源在同一个 asyncio.run 生命周期内建立和关闭;阻塞工作放入受控线程或子进程。单纯把 lease 调长,只会把误判问题藏得更久。
8. 把 TTS 的 UNKNOWN 留在状态机里
音频阶段开始出现直接的外部成本。最危险的状态不是明确失败,而是请求已经发出去,本地却不知道发生了什么。
首个 TTS 适配器选用 qwen3-tts-flash。按 2026 年 10 月 25 日核验的非实时 TTS 文档,单次文本上限为六百字符,生成音频 URL 的有效期为二十四小时。我先标准化术语读法,再按句切成最多四百字符的片段;单个长句继续切分。这个限制只管单次请求,不能代替整期五分钟验收。
每次物理调用先写一行 provider_calls,记录输入指纹和 call_seq,再发请求。收到成功回执,先保存 request ID、音频 URL、过期时间与 usage,再下载、校验、转存。
两种失败需要分开:已经拿到 URL 但下载失败,应复用原 URL;没有收到回执的超时,则可能已经合成并计费。旧 worker 留下的 INTENT 也不能直接解释成“还没发送”,因为它可能正好死在发请求与写回执之间。
我没有依赖一个尚未核验的“按 request_id 查询合成结果”接口。request ID 用于追踪,无法确认的调用停在 WAITING_REVIEW,由用户明确重试缺失片段。新调用增加新的 call_seq,旧 UNKNOWN 记录保留。应用可以守住不重复发布,但无法仅凭自己的幂等键保证供应商零重复计费。
已成功的片段直接复用。第三段失败时不重做前两段,下载失败时不重做合成,发布失败时不重做音频。这些恢复边界比“给整个生成函数加 retry”更贴近实际成本。
9. 实际声音决定时长,字节决定文件身份
字数可以帮助控制讲稿,却不能精确决定声音长度。英文缩写、数字读法、停顿和音色都会影响时长。因此我会先统一片段的采样率、声道与 PCM 参数,按讲稿顺序拼接,再编码成最终 MP3。初始格式设为二十四 kHz、单声道、六十四 kbps,后续用真实听感与文件大小调整。
每个片段记录 segment_index、讲稿跨度和标准化口播文本 hash。合并前检查覆盖范围和顺序,避免漏一句或重复一句;最终产物的输入指纹包含有序片段 hash,以及拼接、编码参数版本。
ffprobe用于读取成品信息,另做一次完整解码检查。毫秒使用 Decimal 向上取整,避免把边界外的一小段截掉:
# ffprobe 输出和完整解码结果均在发布事务之外取得。
from decimal import Decimal, ROUND_CEILING
seconds = Decimal(probe_result["format"]["duration"])
if not seconds.is_finite() or seconds <= 0:
raise InvalidAudio("missing or invalid duration")
duration_ms = int((seconds * 1000).to_integral_value(
rounding=ROUND_CEILING
))
qualified = decode_ok and duration_ms <= 300_000
超长时最多缩稿一次,重新核验关键陈述后再合成。第二个完整合成 pass 仍超长,就保留待处理稿。删掉“远端结果未知”可能使重试案例更短,却会破坏它最重要的结论,所以缩稿必须和证据检查一起做。
文件身份同样不能只依赖 URL,甚至不能只依赖输入指纹。同样的 TTS 输入可能返回不同字节;两个 attempt 如果都向“输入指纹决定的文件名”写入,会让迟到者覆盖已经提交的音频,数据库唯一约束也救不了对象本身。
我的做法是临时对象 key 包含 call ID 或 attempt ID,正式 key 包含 owner 和实际字节 SHA-256,写入后不覆盖。若 stage_artifacts(owner_id, kind, input_fingerprint) 冲突,读取已经提交的产物,后续使用它的对象 key 和 hash。后来者留下的临时对象只在确认没有引用后清理。
输入指纹描述生成条件:用户范围、依赖产物哈希、模型、提示词、音色、语言、术语表、输出参数和相关处理版本。内容哈希描述实际产出的字节。前者决定能否复用,后者决定文件有没有改变。
音频最终 hash 也不能用对象存储的 ETag 无条件替代。S3 的 ETag 说明区分了上传方式和加密方式的影响;应用直接计算字节 SHA-256,更容易建立一致的校验规则。
10. 发布事务同时保护版本和收听预算
音频 READY 表示内容与声音通过门禁,发布还要检查当天是否安排得下。生成前已经做过预留,发布时仍要复核,因为任务执行期间计划可能已经发生合法变化。
一天的计划按用户时区定义,并保存当天预算和偏好快照。新节目先预留三百秒,旧节目复用则占用真实时长。听完、刷新页面和重新排序不会退还已分配额度。初始六百秒预算通常安排一到两条,生成得更短释放了预留差额,也不必继续把它填满。
首版用两类记录表示发布:episodes.published_revision_id 保存全局固定发布版本;plan_items 保存这期在某天被安排收听的事实。同一 episode 可以跨日复用,但同日只出现一次。RSS 从已发布 episodes 生成,不直接展开所有 plan items,否则昨日未听内容重排到今天会变成“新一期”。
发布操作的缩减伪码如下,音频解码、文件 hash 和 ffprobe 已在事务外完成:
def publish_episode(plan_id, episode_id, requested_revision_id,
run_id=None, caller_attempt_id=None):
with transaction():
plan = lock_daily_plan(plan_id)
episode = lock_episode(episode_id)
run = lock_run_if_needed(run_id)
check_owner_and_plan_version(plan, episode)
reject_if_withdrawn(episode)
revision = fixed_published_or_requested_ready_revision(
episode, requested_revision_id
)
reject_conflicting_revision_request(episode, requested_revision_id)
require_current_revision_for_first_publish(episode, revision)
if caller_attempt_id is not None:
require_current_attempt_and_live_lease(run, caller_attempt_id)
validate_audio_hash_and_receipt(revision)
require(0 < revision.duration_ms <= 300_000)
if already_available(plan, episode):
return existing_result(plan, episode)
settle_reservation_and_check_total(plan, revision.duration_ms)
set_publication_pointer_once(episode, revision)
make_plan_item_available(plan, episode, revision.duration_ms)
if run is not None:
append_state_event(run)
这里的 caller_attempt_id 来自可信的 worker 执行上下文;人工重新安排已发布节目时,不要求一个仍然存活的生成 lease。首次发布必须使用当前 revision,worker 发起的状态推进还要复核 attempt 与 lease。
计划锁让并发发布按顺序重新看预算;episode 锁防止两个日期的任务同时首次发布一个节目。已发布内容只能复用固定的 published_revision_id 及其时长,不能保留旧指针却拿另一 READY 版本结算。PostgreSQL 的约束说明也解释了为何普通 CHECK 不能可靠地表达这种跨行聚合约束。
发布后,GUID 和 revision 音频地址固定。事实需要纠正时创建关联 supersedes_episode_id 的新节目,原条保留纠正标记或撤回。已经下载的文件无法假装被用户悄悄换掉,版本变化应该对用户可见。
私有 RSS 的 enclosure 指向自己的稳定媒体入口,带实际字节数与 audio/mpeg 类型;不能直接填二十四小时后过期的供应商 URL。RSS 2.0 规范定义了 GUID 和 enclosure 的语义。媒体路由校验订阅 token、owner 和固定发布版本,并显式实现单段 Range,让播客客户端能够拖动和继续下载;普通流式响应不会自动替应用完成这些 HTTP 语义。
11. 页面进度和学习反馈分别记录
后台任务不会因为用户关掉页面而结束。页面用 SSE 接收阶段状态,重连后从 Last-Event-ID 继续。事件内容是“正在补充资料”“第二段音频已保存”等可解释结果,不传模型内部思维链。
这里有个容易忽略的数据库细节:全局 sequence 的分配顺序不是事务提交顺序。事务 A 先拿到 41 但迟迟未提交,事务 B 的 42 已经被页面看见;若页面以 42 为游标,A 之后提交的事件就可能被跳过。我按 run 锁定任务行,在同一个短事务内更新状态、分配该 run 的 seq 并插入事件。
SSE 每次查询 seq > cursor,页面按 run 和 seq 去重。游标过期时返回状态快照和新游标,两者必须来自同一次一致读。整条 SSE 连接不占住一个长事务。相关协议字段可以对照 MDN 的 SSE 说明。
播放行为另存记录。网页合并真实播放时间区间,拖到结尾不自动算听完;RSS 文件被下载,只说明交付发生。显式反馈则触发具体动作:“浅一点”改变后续解释基础,“再举例”创建针对当前问题的短补充,“继续深入”创建关联原节目与 claim 的续篇候选。
我暂时不把一次听完推断成掌握,也不把一次跳过写成永久兴趣。它们携带的证据强度不同,保留原始反馈事件,比急着压缩成一个长期用户标签更容易纠正。
12. 先设计反例,再决定增强是否值得
这条链路里的每个增强都会增加成本:扩搜多了网络请求,核源多了模型调用,版本和恢复多了状态。它们是否值得,需要通过可以复查的对照回答。
我准备固定四十个 case,其中三十个资料可读,十个包含解析或运行异常;按原始文章分组,划成二十四个开发样本和十六个冻结验证样本,避免同一文章的近似问题分散到两组。每个正常 case 保存原文快照、用户问题、必讲知识点、支持片段和不该出现的断言。
内容实验比较三组:A 直接摘要,B 单问题讲解加有界补证,C 在 B 上增加陈述核源与修订。固定模型、声音和时长规格;再做关闭搜索、关闭一路召回的小规模消融。三组从可读样本中使用同一批二十个代表 case 生成音频,异常样本只验证停止和恢复,不混进听感质量分母。
工程侧先准备这组故障矩阵:
| 注入位置 | 应保留的事实 | 预期验证 |
|---|---|---|
| 业务提交后、入队前退出 | QUEUED run | dispatcher 可以补投 |
| 重复投递同一个 run | 唯一活动 attempt | 只有持有执行权者推进状态 |
| 旧 worker 在接管后返回 | 新 attempt 的活动产物 | 迟到结果不能覆盖 |
| TTS 已成功、下载失败 | 回执和有效 URL | 复用原 URL,不重新合成 |
| TTS 已发出但没有回执 | UNKNOWN 调用记录 | 不自动假定失败并重发 |
| 对象上传成功、PG 提交失败 | 不可变字节与对象身份 | 重试可核验复用 |
| 音频合格、发布事务失败 | READY 产物 | 只重试发布 |
| 两条节目并发发布 | 同一计划预算 | 汇总仍在上限内,GUID 不重复 |
检索看 Recall@8:前八条覆盖的必要标注证据,占全部必要证据的比例。它不等于“至少命中一条”的 Hit@8,更不等于讲稿准确率。讲稿看关键点覆盖和无依据重要陈述;交付看真实时长、版本与预算约束;理解则通过少量复述和相似场景解释观察。
成本也分母明确。每条 READY 成本、每条 PUBLISHED 成本、每条被明确认为有帮助的节目成本,回答不同问题。失败、缩稿和 UNKNOWN 后重试的费用都留在分子里。至于核源究竟减少多少错误、用户是否持续收听,要等这些实验和真实使用完成后再下结论。
13. 我的实现顺序与暂时保留的取舍
我会先做一条能完整走通的路径:提交真实文章,保存快照,直接摘要,分段 TTS,转存、验时长并在网页播放。随后补证据图和核源,再补后台恢复,最后接每日预算、固定发布和私有 RSS。每一步留下正常样例、失败样例、配置与 Trace,避免直到所有模块拼完才第一次检查端到端行为。
公开项目给了这条路径很多参考。AI Podcast Studio帮助理解内容筛选、脚本与发布分阶段;Open Notebook 的播客路由和后台命令提供了请求与生成任务分离的阅读入口;Podcastfy适合放在独立环境做生成效果基线。我当前选择独立业务服务,只借鉴流程与接口分层,尚未声称整体二开了这些仓库。
文本生成与核源先用 qwen-plus 非思考模式,JSON Object 加业务校验;Embedding 先用 text-embedding-v4、1024 维;TTS 使用 qwen3-tts-flash、Cherry、Chinese。模型别名可能变化,run 记录模型与提示词配置,回归前核对版本。
任务初始为 prefork 并发二,首次执行后最多三次业务重试,总计四个 attempt;只对明确可重试错误自动重试。搜索、修稿和 TTS pass 的预算始终累计。以上是实施起点,调整需要留下样本和配置依据。
我暂时没有把多 Agent、GraphRAG、重排服务、实时语音和复杂推荐系统放进首版。当前最需要验证的是:围绕用户的问题补一点恰当背景,是否比普通摘要更有用;新增机制带来的质量差异,是否值得它的调用和维护成本。
这次方案拆解让我更重视“什么时候一个结果才算属于用户”。模型写完讲稿,还要检查证据;TTS 返回成功,还要拿到完整文件;音频通过验收,还要进入正确的预算与版本;页面出现播放按钮,也不能推断用户已经理解。把这些边界逐个写清楚,后续实现才有机会在一次失败之后继续前进,并且解释得清它究竟保存了什么。
评论 0
还没有评论,欢迎留下第一个想法。