fix(dispatcher): 报告 LLM 调用加单次超时上限,治偶发卡死
之前 writeSection/planOutline/planItems 的 pool.Chat 用无 deadline 的 ctx,个别 DeepSeek 流连接挂住会一直占着(曾累积把整篇卡死)。给每次 LLM 调用套 60s 超时 (llmCtx):超时即 cancel → 底层 http 请求中断 → Chat 返回错误 → 走兜底,不无限等。 happy path(约18s 完成)不变,仅加上限。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -99,7 +99,7 @@
|
||||
|
||||
- [ ] 6 个提交待 push(`5d76652` → `79f9912`,需在普通终端 `git push origin main`)
|
||||
- [ ] PDF 导出 Wails 真机验证(不行则回退后端内嵌 CJK 字体出 PDF)
|
||||
- [ ] 报告生成并发健壮性(writeSections 降并发 / 加单次超时,治偶发卡顿)
|
||||
- [x] 报告生成并发健壮性(每次 LLM 调用 60s 超时上限,挂死自释放;规划/分章/撰写均套)
|
||||
- [ ] MinIO 大文档改名/删除的孤儿对象 GC
|
||||
- [x] `make test` 目标(test-go / test-web / test-py 一键跑)
|
||||
|
||||
|
||||
@@ -18,8 +18,15 @@ import (
|
||||
const (
|
||||
reportFanout = 4 // 章节并行撰写的最大并发
|
||||
reportRenderWait = 12 * time.Second // 渲染 docx 的等待上限
|
||||
reportLLMTimeout = 60 * time.Second // 单次 LLM 调用(规划/撰写一章)的超时上限
|
||||
)
|
||||
|
||||
// llmCtx 给单次报告 LLM 调用套超时,避免个别请求挂死拖垮整篇(曾遇连接累积卡死)。
|
||||
// 超时即 cancel → 底层 http 请求中断 → Chat 返回错误 → 调用方走兜底,不无限等待。
|
||||
func llmCtx(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, reportLLMTimeout)
|
||||
}
|
||||
|
||||
// reportOutline 是规划阶段产出的大纲。
|
||||
type reportOutline struct {
|
||||
Title string `json:"title"`
|
||||
@@ -105,7 +112,9 @@ func (o *Orchestrator) planOutline(ctx context.Context, topic string) reportOutl
|
||||
sys := "你是资深报告撰稿人,擅长搭建清晰的报告结构。"
|
||||
user := fmt.Sprintf("请为主题《%s》规划一份报告大纲。"+
|
||||
"只输出 JSON:{\"title\":\"报告标题\",\"sections\":[\"章节标题\", ...]},3 到 5 章,不要任何多余文字。", topic)
|
||||
txt, err := o.pool.Chat(ctx, []llm.ChatMessage{{Role: "system", Content: sys}, {Role: "user", Content: user}})
|
||||
cctx, cancel := llmCtx(ctx)
|
||||
defer cancel()
|
||||
txt, err := o.pool.Chat(cctx, []llm.ChatMessage{{Role: "system", Content: sys}, {Role: "user", Content: user}})
|
||||
if err != nil {
|
||||
log.Printf("[report] 规划大纲失败,用兜底大纲: %v", err)
|
||||
return fallback
|
||||
@@ -134,7 +143,9 @@ func (o *Orchestrator) planItems(ctx context.Context, topic, splitBy string) []s
|
||||
}
|
||||
user := fmt.Sprintf("请把主题《%s》拆分为一组「%s」,用于并行撰写。"+
|
||||
"只输出 JSON 数组:[\"子项1\",\"子项2\", ...],3 到 6 项,不要任何多余文字。", topic, hint)
|
||||
txt, err := o.pool.Chat(ctx, []llm.ChatMessage{
|
||||
cctx, cancel := llmCtx(ctx)
|
||||
defer cancel()
|
||||
txt, err := o.pool.Chat(cctx, []llm.ChatMessage{
|
||||
{Role: "system", Content: "你擅长把一个任务拆解为可并行处理的若干子项。"},
|
||||
{Role: "user", Content: user},
|
||||
})
|
||||
@@ -193,7 +204,9 @@ func (o *Orchestrator) writeSection(ctx context.Context, topic, kb, heading stri
|
||||
ub.WriteString("\n")
|
||||
}
|
||||
ub.WriteString("请就「本章标题」撰写 200–400 字正文。只输出正文,不要重复标题、不要再列提纲。")
|
||||
txt, err := o.pool.Chat(ctx, []llm.ChatMessage{{Role: "system", Content: sys}, {Role: "user", Content: ub.String()}})
|
||||
cctx, cancel := llmCtx(ctx)
|
||||
defer cancel()
|
||||
txt, err := o.pool.Chat(cctx, []llm.ChatMessage{{Role: "system", Content: sys}, {Role: "user", Content: ub.String()}})
|
||||
if err != nil {
|
||||
log.Printf("[report] 撰写「%s」失败: %v", heading, err)
|
||||
return "(本章撰写失败:" + err.Error() + ")"
|
||||
|
||||
Reference in New Issue
Block a user