日期: 2026年5月13日——2026年5月29日
项目: 阅见:基于大模型的交互式小说阅读平台


项目实训个人工作记录(七)——剧情推演中编排器的实现

一、概述

本阶段实现了剧情推演子系统的核心调度层——PlotOrchestrator(剧情编排器),代码位于 ai-service/Plot_development/agents/orchestrator.py。编排器负责在每一回合中,根据玩家选择与世界/角色当前状态,动态决定下一步调用世界智能体(WorldAgent)还是某位角色智能体(RoleAgent),并在多步行动结束后整合生成叙事与可选分支。

从 AI 能力角度看,编排器并非单次调模型出一段文字,而是在一回合内串联多次 LLM 调用+向量检索(RAG)+流式叙事生成,形成检索增强 → 决策调度 → 角色/世界行动 → 日志整合 → 选项生成的完整 AI 流水线。WorldAgent 与 RoleAgent 分别走不同的模型调用通道,向量库负责注入书籍设定与用户历史,共同保证推演既贴合原著设定、又具备跨回合连贯性。

前端页面 frontend/src/views/PlotBranch.vue 通过 SSE 流式接口实时展示编排过程:索引构建,智能体逐步行动,叙事整合,下一回合选项。后端 Spring Boot 经 PlotController / PlotService 转发请求至 ai-service 的 /v1/plot/advance/stream,会话状态持久化在 MySQL 表 vr_plot_sessionstate_json 字段中。


二、功能目标

  1. 多智能体回合调度

    • 每回合最多执行 8 步(DEFAULT_MAX_ORCHESTRATOR_STEPS = HARD_MAX_ORCHESTRATOR_STEPS = 8),这里是考虑到单次推演的执行时间与用户体验综合性从而设置的参数,编排器 LLM 每步输出 { next, role_name, hint }next 取值 role / world / finish
    • 角色步:调用 RoleAgent.plan_with_llm 规划行动,支持与环境交互或与其他角色单聊;世界步:调用 WorldAgent.environment_interact 生成环境反馈。
  2. AI 上下文增强(RAG)

    • 推演前自动构建/校验 Chroma 知识索引;每回合按玩家选择拼接 query,检索书籍设定片段本会话历史片段注入 prompt。
    • 回合结束后将本轮叙事写入历史向量库,供后续回合检索。
  3. 流式输出与前端可视化

    • astream_step 按阶段 yield 事件:indexingagentsorchestratoractionnarrative(content 块)→ done
    • 叙事整合阶段使用 LLM 流式输出(aiter_log2story),其余步骤为同步调用。
  4. 容错与兜底

    • 世界/角色数据缺失、LLM 未配置时返回 offline 响应,不中断页面。
    • 编排器 JSON 解析失败时按 scene_actors 轮询降级;本回合尚无输出时不允许直接 finish_coerce_orchestrator_decision)。

三、技术实现

(一)数据与依赖

存储 作用 在编排器中的使用点
vr_plot_session 用户×书籍推演会话,state_json 存回合 state + 分支树 PlotService.getOrCreateSession 读取/写回;编排器入参 state_json,出参 state
Plot_development/data/ 世界/地点/角色运行时数据 resolve_plot_preset(book_id) 加载 world_descriptionrole_nameslocation
Chroma vistaread_plot_knowledge 书籍设定向量索引 _retrieve_vectorsknowledge_indexer.search_formatted
Chroma vistaread_plot_history 用户推演历史向量 _finalize_step 写入;检索时排除本回合 recent_snippets 防重复
state_jsonbranch_tree 多分支时间线 resolve_advance / append_turn_node 支持回溯分叉

state 核心字段:turnsummaryeventlogs(智能体行动日志)、agents(各 RoleAgent 持久化状态)、scenelast_choicesbranch_tree


(二)编排器如何调用 AI(重点)

1. AI 基础设施:双通道模型调用

编排器在 main.py 中通过 _make_plot_orchestrator() 组装,注入三类 AI 依赖:

PlotOrchestrator(
    llm=LlmClient(),                    # 世界智能体 + 编排器决策 + 角色规划
    llm_caller=_call_chat_model,        # 角色对话/回应(带 system prompt 与人设)
    knowledge_indexer=plot_knowledge_indexer,
    history_store=plot_history_store,
)
通道 实现 主要用于 特点
LlmClient llm_client.py,OpenAI 兼容 /chat/completions WorldAgent 全部能力;编排器 _orchestrator_decide;RoleAgent plan_with_llm 可配置 LLM_MODEL / LLM_MODEL_WORLD;支持 iter_chat / aiter_chat 流式;默认 thinking: disabled
llm_caller main.py_call_chat_model RoleAgent respond / single_role_interact 注入 _build_role_system_prompt(角色人设、关系、动机);默认开启 thinking 模式

环境变量(节选):
  • LLM_API_BASE / LLM_API_KEY / LLM_MODEL:对话模型(默认兼容 DashScope)
  • LLM_MODEL_WORLD:世界智能体专用模型,未配置时回退 LLM_MODEL
  • EMBEDDING_MODEL:向量模型(默认 text-embedding-v2
  • CHROMA_PLOT_KNOWLEDGE_COLLECTION / CHROMA_PLOT_HISTORY_COLLECTION:两个 Chroma 集合名

未配置 LLM_API_KEY 时,llm.configured == False_prepare_step 直接返回 offline 响应,不发起任何模型请求。

2. RAG:Embedding + Chroma 双库检索

推演 AI 的「长期记忆」分两层,均在 ai-service 启动时初始化(main.py lifespan):

EmbeddingService (dashscope embedding)
    ├── PlotKnowledgeIndexer → vistaread_plot_knowledge  (按 book_id,全员共享设定)
    └── PlotHistoryStore     → vistaread_plot_history      (按 user_id + book_id + session_id)

知识库构建(非 LLM,纯 Embedding)

  • 首次推演前 astream_step 会 yield phase: indexing,调用 knowledge_indexer.ensure_index(book_id)
  • data/worlds/data/locations/data/maps/data/roles/*/role_info.json 切分 chunk,批量 embedding.embed() 写入 Chroma。
  • 通过 data/.index_manifest/book_{id}.json 比对源文件 mtime,仅在数据变更时重建。

回合内检索(RAG 注入点)

_prepare_step 拼接检索 query:

search_query = choice + event + location + summary[:200]

调用 _retrieve_vectors 得到两段格式化文本,经 _append_retrieved_context 拼入 WorldAgent 的 update_event 历史上下文,以及编排器 ORCHESTRATOR_DECIDE{retrieved_knowledge} / {retrieved_history} 占位符:

检索源 方法 top_k 截断 过滤
设定知识库 knowledge_indexer.search_formatted(book_id, query) 5 1600 字符 book_id
推演历史 history_store.search_formatted(..., exclude_snippets=logs[-8:]) 5 3000 字符 user + book + session;排除本回合已写 log 防重复

历史回写(回合结束后)

_finalize_step 调用 history_store.add_turn():将 turn、玩家选择、场景、角色、事件、旁白摘要拼成 document → embedding.embed() → upsert 至 Chroma,供下一回合编排器决策时检索。

3. 单回合 AI 调用全景

一回合完整推演中,LLM 调用次数随编排步数动态变化(理论上限约 2 + 8×(1~3) + 3 ≈ 30+ 次,实际通常 10~20 次)。按流水线顺序如下:

[可选] ensure_index(仅 Embedding)
    ↓
① update_event          — WorldAgent,LlmClient,temp=0.5
    ↓
② orchestrator_decide   — 编排器,LlmClient,temp=0.35,JSON 决策(每步 1 次,最多 8 次)
    ↓
③ role_plan             — RoleAgent,LlmClient,temp=0.65,JSON 行动方案(每 role 步 1 次)
    ↓
④a single_role_interact — RoleAgent,llm_caller,角色人设对话(interact_type=single 时)
④b environment_interact — WorldAgent,LlmClient,temp=0.6,环境反馈文本
    ↓(循环 ②~④ 直至 finish 或达 max_steps)
⑤ log2story             — WorldAgent,LlmClient,temp=0.65,流式叙事(astream 路径)
⑥ generate_choices      — WorldAgent,LlmClient,temp=0.7,JSON 数组 2~4 项
⑦ judge_if_ended        — WorldAgent,LlmClient,temp=0.3,JSON {if_end, detail}
    ↓
[Embedding] history_store.add_turn — 写入向量库

4. 各 AI 调用点详解

(1)编排器决策 _orchestrator_decide
  • Prompt 模板prompts/plot_zh.pyORCHESTRATOR_DECIDE
  • 输入:世界描述、场景、当前事件、玩家选择、在场角色列表、本回合 logs_round_logs,不含历史回合)、RAG 检索结果、当前步/最大步
  • 输出解析_parse_orchestrator_decision 从模型回复中提取 JSON,next ∈ {role, world, finish}
  • 温度 0.35:偏低,保证调度稳定、格式可解析
  • 业务纠偏_coerce_orchestrator_decision 在代码层约束 AI——本回合尚无智能体输出时不允许 finish,改为轮询 scene_actors;JSON 解析失败时同样降级为按角色名单轮流调用
(2)世界智能体 WorldAgent(LlmClient + model_world)
方法 Prompt temperature 输出
update_event UPDATE_EVENT 0.5 一句更新后的事件摘要
environment_interact ENVIRONMENT_INTERACT 0.6 环境反馈纯文本
log2story / aiter_log2story LOG2STORY 0.65 150~280 字第三人称叙事;流式逐 chunk 推送
generate_choices GENERATE_CHOICES 0.7 JSON 数组,2~4 个后续选项
judge_if_ended JUDGE_IF_ENDED 0.3 JSON {if_end, detail}

LOG2STORY 明确要求承接「上一轮旁白」、禁止无故重置场景,是将多步智能体 logs 二次加工为读者可读叙事的关键 AI 环节;流式路径下通过 aiter_log2story → SSE content 事件推送到前端。

(3)角色智能体 RoleAgent(双模型分工)

规划阶段 plan_with_llm(LlmClient)

  • Prompt:ROLE_PLAN,注入角色目标、状态、其他在场角色、历史(含编排 hint)
  • 输出 JSON:{ interact_type, detail, target_role_name, action }
  • interact_type == "single":触发目标角色的对话 AI;否则走世界环境反馈

回应阶段 single_role_interact(llm_caller)

  • 不走 plot_zh.py 模板,而是 _call_chat_model(mode="character", ...)
  • System prompt 由 _build_role_system_prompt 动态构建:角色名 + role_info.json 中的 identity / relations / motivation_goal
  • 将「与行动者的关系说明 + 对方刚刚的行动」写入 extra_context,让模型以该角色口吻回应
  • 默认 thinking 模式开启,适合需要推理的角色反应;规划 JSON 仍走 LlmClient 且 thinking disabled
(4)AI 输出后处理

所有智能体输出的 detail / 环境反馈 / 回复文本,经 text_sanitize.sanitize_plot_agent_text 清洗:

  • 剥离 ... 思考链残留
  • 去除 markdown 代码块、JSON 字段泄漏
  • 规范化换行与零宽字符

避免模型偶发输出 JSON 或思考过程污染前端时间线展示。

5. 流式 vs 同步:AI 结果如何到达前端

阶段 AI 调用方式 SSE 事件
知识索引 Embedding(线程池) { phase: "indexing", message }
事件更新 WorldAgent.update_event 同步 { action: { actor:"world", act_type:"event" } }
编排调度 _orchestrator_decide 同步 { phase: "orchestrator" } / { phase: "agents", stage: "step" }
角色/世界行动 plan + interact 同步 { action: { actor, act_type, detail } }
叙事整合 aiter_log2story 流式 { phase: "narrative" } + 多次 { content: "..." }
收尾 choices / ended 同步 { done: true, narrative, choices, state }

编排器在 _run_orchestrated_phase 中将同步 LLM 调用包在 asyncio.to_thread 内,避免阻塞 FastAPI 事件循环;仅叙事阶段使用真正的 async 流式 iterator。

6. AI 降级策略小结

场景 处理方式
LLM 未配置 offline 响应 + 固定占位选项
知识索引失败 done.error + offline: true
编排器 JSON 无效 按 scene_actors 轮流调用 role
本回合无输出即 finish _coerce_orchestrator_decision 强制 role
role_plan 异常 默认 { interact_type: "environment", detail: "环顾四周" }
log2story 超时/失败 回退为 logs 原文或「第 N 轮:你选择了…」
generate_choices 失败 固定三个通用选项

(三)编排器核心流程(PlotOrchestrator)

1. 准备阶段 _prepare_step

  • 校验 preset.has_world / preset.has_roles,缺失则返回 offline 错误提示。
  • 执行 RAG 检索,实例化 RoleAgent,调用 WorldAgent.update_event第 1 次 LLM)。

2. 调度循环 _orchestrator_decide + _run_orchestrated_phase

编排器 prompt 定义于 ORCHESTRATOR_DECIDE,每步一次 LLM 决策,驱动 role/world 行动子流程(见上一节调用全景)。

防空回合机制:若 LLM 返回 finish 但本回合 _round_logs 为空,强制改为调用 scene_actors 中第一位角色。

3. 角色行动 _execute_role_action

  • RoleAgent.plan_with_llm → 按 interact_type 分支至 single_role_interactWorldAgent.environment_interact
  • 所有 detail 经 sanitize_plot_agent_text 清洗后写入 logs。

4. 收尾 _finalize_step

  • 流式/同步 log2storygenerate_choicesjudge_if_endedhistory_store.add_turn(Embedding 写入)。
  • 输出 state 合并分支树。

5. 双模式入口

方法 用途
run_step 同步整轮推演,POST /v1/plot/advance
astream_step 异步流式,POST /v1/plot/advance/stream,叙事阶段 SSE 推送 content

(四)后端桥接层

PlotController(前缀 /api/plot):

  • GET /{bookId}/session:获取/创建会话
  • POST /{bookId}/advance/stream:流式推演(前端主路径)
  • POST /{bookId}/branch/goto:分支树节点跳转

PlotService 组装请求体,经 AiStreamingService.streamPlot 转发至 ai-service;done 事件中 state 回写 vr_plot_session


(五)前端实现(PlotBranch.vue)

1. 流式消费 pick(choice)

  • 解析 SSE:phase 更新状态栏;action 追加智能体时间线;content 流式写入叙事;done 更新 state/choices。

2. 智能体回合 UI

  • agent-round 消息块展示编排器调度过程;narrativeStarted 后锁定 action 追加,仅消费 content 流。

3. 分支与回溯

  • 回溯节点传入 branchFromNodeId,编排器 resolve_advance 从该节点分叉,AI 上下文仍走同一套 RAG + 决策流程。

(六)业务流程总结

  1. 用户进入剧情推演页,加载 vr_plot_session.state_json
  2. 用户点击选项 → ensure_index(Embedding)→ prepare(RAG + update_event)→ 编排器多步 LLM 调度 → 流式 log2story → generate_choices → 历史写入 Chroma。
  3. 下一回合检索到本轮写入的历史片段,AI 决策时可「想起」此前旁白与选择。
  4. 用户回溯分叉时,state 切换至历史节点 snapshot,后续推演重新走完整 AI 流水线。

四、问题反馈

1. 编排器过早 finish 导致空回合

现象: LLM 直接返回 finish,本回合无智能体输出。

处理: _coerce_orchestrator_decision 代码层纠偏 + 循环结束后兜底执行 scene_actor。单测:test_coerce_finish_when_round_logs_empty

2. 向量检索与当前 logs 重复

现象: RAG 注入内容与刚产生的 log 高度重合。

处理: exclude_snippets=logs[-8:] 过滤近期片段。

3. 角色回应出现思考链或 JSON 泄漏

现象: 时间线卡片出现 `` 或 "detail": "..." 原文。

处理: sanitize_plot_agent_text 统一清洗;规划类调用使用 LlmClient 且 thinking disabled。

4. 流式阶段与叙事块时序错乱

现象: 叙事 content 出现在智能体步骤之前。

处理: PlotBranch.vueparsed.action 仅在 !narrativeStarted 时 append。


五、界面展示

在这里插入图片描述剧情推演功能展示

在这里插入图片描述

在这里插入图片描述

页面左侧右侧为分支脉络面板;状态栏显示「编排器正在调度智能体…」「正在整合叙事…」等阶段提示。


六、优化思路

  1. 模型分级:编排器决策、JSON 结构化输出走轻量模型;log2story 走更强模型,降低延迟与成本。
  2. RAG query 优化:除 choice+event 外,可加入上一回合 narrative embedding 做 hybrid 检索。
  3. Thinking 模式按需开关:规划/调度关闭 thinking,仅角色深度对话开启,减少 token 消耗。
  4. 编排器决策缓存:相似 state 下缓存最近决策(需注意剧情多样性)。

七、总结

本阶段我完成了剧情推演编排器 PlotOrchestrator 的端到端落地。从 AI 视角看,其核心是:以 Embedding+RAG 注入设定与历史上下文,以编排器 LLM 做多步 {next, role_name, hint} 调度,以 WorldAgent/RoleAgent 分工完成事件更新、行动规划、角色对话与环境反馈,最终以流式 log2story 将离散 logs 整合为连贯叙事,并回写历史向量供下回合检索。双通道模型调用(LlmClient + llm_caller)、分阶段 temperature 控制、JSON 解析纠偏与 sanitize 后处理,共同保证了多轮、多智能体场景下 AI 输出的可用性与体验一致性。


AI 提示词记录(节选):

请为「剧情推演编排器 PlotOrchestrator」设计多智能体调度方案:每回合由编排器 LLM 决定下一步调用 role/world/finish,RoleAgent 负责 plan + 交互,WorldAgent 负责 event 更新、环境反馈与 log2story 叙事整合;需支持 SSE 流式、Chroma 向量上下文、分支树 state 持久化。

实现 ORCHESTRATOR_DECIDE prompt 与 _coerce_orchestrator_decision 防空回合;前端 PlotBranch.vue 消费 phase/action/content/done 事件,展示 agent-round 时间线。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐