simulation-engine.ts 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  1. import type { ChatMessage } from "@/lib/llm-client"
  2. import { streamChat } from "@/lib/llm-client"
  3. import type { LlmConfig } from "@/stores/wiki-store"
  4. import {
  5. buildAgentContext,
  6. getVisibleEvents,
  7. } from "@/lib/novel/story-simulation/agent-profile-builder"
  8. import type {
  9. ActionVisibility,
  10. AgentAction,
  11. AgentActionType,
  12. EventImpact,
  13. ExtractionResult,
  14. NovelAgent,
  15. SimulationEvent,
  16. SimulationInput,
  17. SimulationState,
  18. StoryNode,
  19. TimelineEvent,
  20. } from "@/lib/novel/story-simulation/types"
  21. import { calcMaxRoundsPerNode } from "@/lib/novel/story-simulation/types"
  22. // ── 对外接口 ──
  23. export interface SimulationCallbacks {
  24. onEvent: (event: SimulationEvent) => void
  25. onProgress: (progress: number, label: string) => void
  26. onComplete: (events: SimulationEvent[]) => void
  27. onError: (error: Error) => void
  28. /** 新引擎时间线事件回调 */
  29. onTimelineEvent?: (event: TimelineEvent) => void
  30. }
  31. // ── 常量 ──
  32. const MAX_ROUNDS_PER_NODE_FALLBACK = 3
  33. const REACT_CHAIN_LIMIT = 2
  34. // ── 内部辅助:将 streamChat 的流式回调收拢为一个完整字符串 ──
  35. async function collectStream(
  36. config: LlmConfig,
  37. messages: ChatMessage[],
  38. signal?: AbortSignal,
  39. ): Promise<string> {
  40. let result = ""
  41. let streamError: Error | null = null
  42. await streamChat(
  43. config,
  44. messages,
  45. {
  46. onToken: (token) => {
  47. result += token
  48. },
  49. onDone: () => {},
  50. onError: (err) => {
  51. streamError = err
  52. },
  53. },
  54. signal,
  55. )
  56. if (streamError) throw streamError
  57. return result
  58. }
  59. // ── 内部辅助:Agent 深拷贝(避免事件间共享可变状态) ──
  60. function cloneAgent(agent: NovelAgent): NovelAgent {
  61. return {
  62. ...agent,
  63. knownFacts: new Set(agent.knownFacts),
  64. relationships: new Map(
  65. Array.from(agent.relationships.entries()).map(([k, v]) => [
  66. k,
  67. { ...v },
  68. ]),
  69. ),
  70. memory: {
  71. observedEvents: [...agent.memory.observedEvents],
  72. knownSecrets: new Set(agent.memory.knownSecrets),
  73. sentiments: new Map(agent.memory.sentiments),
  74. recentDecisions: [...agent.memory.recentDecisions],
  75. },
  76. knowledgeScope: [...agent.knowledgeScope],
  77. personality: [...agent.personality],
  78. }
  79. }
  80. function cloneAgentsToMap(agents: NovelAgent[]): Map<string, NovelAgent> {
  81. const map = new Map<string, NovelAgent>()
  82. for (const a of agents) {
  83. map.set(a.characterId, cloneAgent(a))
  84. }
  85. return map
  86. }
  87. // ── 内部辅助:生成唯一 ID ──
  88. let eventCounter = 0
  89. function nextEventId(): string {
  90. eventCounter++
  91. return `evt_${Date.now()}_${eventCounter}`
  92. }
  93. // ── 内部辅助:构建 Agent 系统提示词 ──
  94. function buildSystemPrompt(agent: NovelAgent): string {
  95. const personalityLine =
  96. agent.personality.length > 0
  97. ? `你的性格关键词:${agent.personality.join("、")}`
  98. : ""
  99. const styleLine = agent.speakingStyle
  100. ? `你的说话风格:${agent.speakingStyle}`
  101. : ""
  102. return [
  103. `你正在扮演小说中的真实角色「${agent.name}」。你不是AI助手,你就是这个角色本人。`,
  104. "",
  105. "【核心原则 - 必须严格遵守】",
  106. "1. 你是小说中的真实角色,只能基于你知道的信息行动,绝不能使用你不知道的信息。",
  107. "2. 绝对禁止全知视角:你不知道其他角色的内心想法,不知道没有发生在你面前的事情,不知道剧情走向。",
  108. "3. 严格遵循你的性格特征、说话风格和行为逻辑,不要跳出角色。",
  109. "4. 你的每个行为都应该有合理的动机,符合角色设定。",
  110. "",
  111. personalityLine,
  112. styleLine,
  113. "",
  114. "【行为类型说明】你只能选择以下一种行为类型:",
  115. "- evaluate:评价某人或某事,表达你的看法和判断",
  116. "- pushPlot:主动采取推动剧情发展的关键行动",
  117. "- observe:观察周围环境、人物或事态(不改变现状,只是获取信息)",
  118. "- react:对其他角色刚做出的行为做出即时反应",
  119. "- speak:与其他角色对话(说出台词)",
  120. "- ally:寻求结盟、合作、示好",
  121. "- confront:对抗、质疑、挑衅",
  122. "- conceal:隐瞒信息、假装不知道、掩饰真实想法",
  123. "- investigate:调查、探索、打听消息",
  124. "",
  125. "【输出格式】你必须输出一个严格的JSON对象,不要输出任何其他文字,不要使用markdown代码块:",
  126. "{",
  127. ' "type": "行为类型(从上面列表选一个)",',
  128. ' "content": "行为的具体内容/说的话/内心想法",',
  129. ' "target": "目标角色名(可选,没有目标就不填)",',
  130. ' "visibility": "all(所有人可见) 或 target_only(仅目标可见) 或 self(仅自己可见/内心活动)",',
  131. ' "motivation": "你为什么做出这个行为的内心动机",',
  132. ' "plot_push": "这个行为如何推动剧情向节点目标发展"',
  133. "}",
  134. "",
  135. "【可见性规则】",
  136. "- 公开的言行(speak/ally/confront/pushPlot的公开部分)用 all",
  137. "- 私下对话(speak带target)用 target_only",
  138. "- 内心想法(evaluate/observe的心理活动/conceal)用 self",
  139. "",
  140. "只输出JSON对象,不要输出任何其他文字。",
  141. ]
  142. .filter((line) => line !== null && line !== undefined)
  143. .join("\n")
  144. }
  145. // ── 内部辅助:构建用户消息(上下文 + 指令) ──
  146. function buildUserMessage(
  147. context: string,
  148. node: StoryNode,
  149. agentName: string,
  150. injectionEvent?: string,
  151. ): string {
  152. const parts: string[] = [context]
  153. if (injectionEvent) {
  154. parts.push("")
  155. parts.push("【突发事件】")
  156. parts.push(injectionEvent)
  157. }
  158. parts.push("")
  159. parts.push(
  160. `当前是节点「${node.title}」,节点目标是:${node.goal}。请根据以上信息,以「${agentName}」的视角决定你接下来要做的一个行为,并严格按JSON格式输出。`,
  161. )
  162. return parts.join("\n")
  163. }
  164. // ── 内部辅助:从 LLM 文本中提取 JSON ──
  165. function extractJson(text: string): string | null {
  166. const trimmed = text.trim()
  167. // 直接解析
  168. try {
  169. JSON.parse(trimmed)
  170. return trimmed
  171. } catch {
  172. // 继续
  173. }
  174. // 从 markdown 代码块中提取
  175. const codeBlockMatch = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed)
  176. if (codeBlockMatch) {
  177. const candidate = codeBlockMatch[1].trim()
  178. try {
  179. JSON.parse(candidate)
  180. return candidate
  181. } catch {
  182. // 继续
  183. }
  184. }
  185. // 从文本中查找第一个 JSON 对象
  186. const objMatch = /\{[\s\S]*\}/.exec(trimmed)
  187. if (objMatch) {
  188. const candidate = objMatch[0]
  189. try {
  190. JSON.parse(candidate)
  191. return candidate
  192. } catch {
  193. // 继续
  194. }
  195. }
  196. return null
  197. }
  198. // ── 内部辅助:验证行为类型是否合法 ──
  199. const VALID_ACTION_TYPES: AgentActionType[] = [
  200. "evaluate",
  201. "pushPlot",
  202. "observe",
  203. "react",
  204. "speak",
  205. "ally",
  206. "confront",
  207. "conceal",
  208. "investigate",
  209. ]
  210. function isValidActionType(t: string): t is AgentActionType {
  211. return (VALID_ACTION_TYPES as string[]).includes(t)
  212. }
  213. function isValidVisibility(v: string): v is ActionVisibility {
  214. return v === "all" || v === "target_only" || v === "self"
  215. }
  216. // ── 内部辅助:解析 LLM 输出为行为(健壮:失败时作为 observe 处理) ──
  217. interface ParsedAction {
  218. action: AgentAction
  219. motivation: string
  220. plotPush: string
  221. visibility: ActionVisibility
  222. }
  223. function parseAgentAction(raw: string): ParsedAction {
  224. const fallback: ParsedAction = {
  225. action: {
  226. type: "observe",
  227. content: raw.trim().slice(0, 200) || "沉默地观察周围",
  228. visibility: "self",
  229. },
  230. motivation: "",
  231. plotPush: "",
  232. visibility: "self",
  233. }
  234. const jsonText = extractJson(raw)
  235. if (!jsonText) {
  236. return fallback
  237. }
  238. try {
  239. const data = JSON.parse(jsonText) as Record<string, unknown>
  240. const typeRaw = String(data.type ?? "observe").toLowerCase()
  241. const type: AgentActionType = isValidActionType(typeRaw)
  242. ? typeRaw
  243. : "observe"
  244. const content = String(data.content ?? "").trim() || fallback.action.content
  245. const target =
  246. data.target !== undefined && data.target !== null && data.target !== ""
  247. ? String(data.target)
  248. : undefined
  249. const visRaw = String(data.visibility ?? "all").toLowerCase()
  250. const visibility: ActionVisibility = isValidVisibility(visRaw)
  251. ? visRaw
  252. : type === "speak" && target
  253. ? "target_only"
  254. : type === "evaluate" || type === "observe" || type === "conceal"
  255. ? "self"
  256. : "all"
  257. const motivation = String(data.motivation ?? "").trim()
  258. const plotPush = String(data.plot_push ?? "").trim()
  259. // 根据行为类型推断默认可见性(LLM未明确指定时)
  260. let finalVisibility = visibility
  261. if (!data.visibility) {
  262. if (type === "evaluate" || type === "observe" || type === "conceal") {
  263. finalVisibility = "self"
  264. } else if ((type === "speak" || type === "ally" || type === "confront") && target) {
  265. finalVisibility = "target_only"
  266. } else {
  267. finalVisibility = "all"
  268. }
  269. }
  270. return {
  271. action: {
  272. type,
  273. content,
  274. target,
  275. visibility: finalVisibility,
  276. motivation: motivation || undefined,
  277. plot_push: plotPush || undefined,
  278. },
  279. motivation,
  280. plotPush,
  281. visibility: finalVisibility,
  282. }
  283. } catch {
  284. return fallback
  285. }
  286. }
  287. // ── 内部辅助:根据名字或 ID 查找目标 Agent ──
  288. function resolveTarget(
  289. targetName: string | undefined,
  290. agents: Map<string, NovelAgent>,
  291. ): NovelAgent | undefined {
  292. if (!targetName) return undefined
  293. for (const agent of agents.values()) {
  294. if (agent.name === targetName || agent.characterId === targetName) {
  295. return agent
  296. }
  297. }
  298. return undefined
  299. }
  300. // ── 内部辅助:根据可见性确定 observableBy 列表 ──
  301. function determineObservableBy(
  302. actor: NovelAgent,
  303. target: NovelAgent | undefined,
  304. visibility: ActionVisibility,
  305. allAgents: Map<string, NovelAgent>,
  306. ): string[] {
  307. switch (visibility) {
  308. case "self":
  309. return [actor.characterId]
  310. case "target_only":
  311. if (target) {
  312. return [actor.characterId, target.characterId]
  313. }
  314. return [actor.characterId]
  315. case "all":
  316. default: {
  317. const ids: string[] = []
  318. for (const id of allAgents.keys()) {
  319. ids.push(id)
  320. }
  321. return ids
  322. }
  323. }
  324. }
  325. // ── 内部辅助:计算事件对角色的影响 ──
  326. function computeImpacts(
  327. actor: NovelAgent,
  328. parsed: ParsedAction,
  329. target: NovelAgent | undefined,
  330. ): EventImpact[] {
  331. const impacts: EventImpact[] = []
  332. const { action } = parsed
  333. // 对目标的情感影响
  334. if (target) {
  335. switch (action.type) {
  336. case "ally":
  337. impacts.push({
  338. characterId: target.characterId,
  339. type: "relationship",
  340. detail: `${actor.name}向${target.name}示好结盟,好感度上升`,
  341. })
  342. break
  343. case "confront":
  344. impacts.push({
  345. characterId: target.characterId,
  346. type: "relationship",
  347. detail: `${actor.name}与${target.name}对抗,好感度下降`,
  348. })
  349. break
  350. case "speak":
  351. impacts.push({
  352. characterId: target.characterId,
  353. type: "knowledge",
  354. detail: `${target.name}听到了${actor.name}说的话`,
  355. })
  356. break
  357. case "react":
  358. impacts.push({
  359. characterId: target.characterId,
  360. type: "sentiment",
  361. detail: `${actor.name}对${target.name}的行为做出了反应`,
  362. })
  363. break
  364. }
  365. }
  366. // 对自己的影响
  367. switch (action.type) {
  368. case "investigate":
  369. case "observe":
  370. impacts.push({
  371. characterId: actor.characterId,
  372. type: "knowledge",
  373. detail: `${actor.name}获取了新信息`,
  374. })
  375. break
  376. case "conceal":
  377. impacts.push({
  378. characterId: actor.characterId,
  379. type: "knowledge",
  380. detail: `${actor.name}隐藏了某些信息`,
  381. })
  382. break
  383. case "evaluate":
  384. impacts.push({
  385. characterId: actor.characterId,
  386. type: "sentiment",
  387. detail: `${actor.name}形成了看法/评价`,
  388. })
  389. break
  390. }
  391. // 公开事件对所有人产生知识影响
  392. if (action.visibility === "all") {
  393. impacts.push({
  394. characterId: "__all__",
  395. type: "knowledge",
  396. detail: `所有人都观察到了${actor.name}的公开行为`,
  397. })
  398. }
  399. return impacts
  400. }
  401. // ── 内部辅助:将事件应用到 Agent 记忆 ──
  402. function applyEventToMemory(
  403. agent: NovelAgent,
  404. event: TimelineEvent,
  405. ): void {
  406. agent.memory.observedEvents.push(event.id)
  407. // 处理影响
  408. for (const impact of event.impacts) {
  409. if (impact.characterId === "__all__" || impact.characterId === agent.characterId) {
  410. if (impact.type === "knowledge") {
  411. // 知识类影响:加入到 knowledgeScope
  412. if (!agent.knowledgeScope.includes(impact.detail)) {
  413. agent.knowledgeScope.push(impact.detail)
  414. }
  415. } else if (impact.type === "sentiment") {
  416. // 情感变化
  417. agent.emotionalState = impact.detail.includes("正面") ? "positive" : agent.emotionalState
  418. } else if (impact.type === "relationship" && event.targetId) {
  419. // 关系变化
  420. const current = agent.memory.sentiments.get(event.targetId) ?? 0
  421. let delta = 0
  422. if (impact.detail.includes("上升") || impact.detail.includes("结盟") || impact.detail.includes("示好")) {
  423. delta = 10
  424. } else if (impact.detail.includes("下降") || impact.detail.includes("对抗") || impact.detail.includes("冲突")) {
  425. delta = -10
  426. }
  427. agent.memory.sentiments.set(
  428. event.targetId,
  429. Math.max(-100, Math.min(100, current + delta)),
  430. )
  431. // 同步到旧 relationships 字段以兼容
  432. const oldRel = agent.relationships.get(event.targetId)
  433. if (oldRel) {
  434. oldRel.sentiment = Math.max(-100, Math.min(100, current + delta))
  435. oldRel.relationType = delta > 0 ? "ally" : delta < 0 ? "hostile" : oldRel.relationType
  436. }
  437. }
  438. }
  439. }
  440. // 记录最近决策(仅对行为发起者)
  441. if (event.actorId === agent.characterId) {
  442. agent.memory.recentDecisions.push(
  443. `[R${event.round + 1}] ${event.actionType}: ${event.content.slice(0, 50)}`,
  444. )
  445. if (agent.memory.recentDecisions.length > 20) {
  446. agent.memory.recentDecisions = agent.memory.recentDecisions.slice(-20)
  447. }
  448. }
  449. }
  450. // ── 内部辅助:创建时间线事件 ──
  451. function createTimelineEvent(
  452. actor: NovelAgent,
  453. parsed: ParsedAction,
  454. target: NovelAgent | undefined,
  455. round: number,
  456. nodeIndex: number,
  457. allAgents: Map<string, NovelAgent>,
  458. ): TimelineEvent {
  459. const observableBy = determineObservableBy(
  460. actor,
  461. target,
  462. parsed.visibility,
  463. allAgents,
  464. )
  465. const impacts = computeImpacts(actor, parsed, target)
  466. return {
  467. id: nextEventId(),
  468. round,
  469. nodeIndex,
  470. actorId: actor.characterId,
  471. actorName: actor.name,
  472. actionType: parsed.action.type,
  473. content: parsed.action.content,
  474. targetId: target?.characterId,
  475. targetName: target?.name,
  476. observableBy,
  477. impacts,
  478. timestamp: new Date().toISOString(),
  479. }
  480. }
  481. // ── 内部辅助:将 TimelineEvent 转换为旧 SimulationEvent(兼容报告生成) ──
  482. function timelineEventToSimulationEvent(
  483. tlEvent: TimelineEvent,
  484. agent: NovelAgent,
  485. parsed: ParsedAction,
  486. node: StoryNode,
  487. round: number,
  488. ): SimulationEvent {
  489. const stateChanges: string[] = []
  490. if (parsed.motivation) {
  491. stateChanges.push(`动机:${parsed.motivation}`)
  492. }
  493. if (parsed.plotPush) {
  494. stateChanges.push(`剧情推动:${parsed.plotPush}`)
  495. }
  496. for (const impact of tlEvent.impacts) {
  497. if (impact.characterId !== "__all__") {
  498. stateChanges.push(impact.detail)
  499. }
  500. }
  501. // 将新行为类型映射为旧 AgentAction 结构(扁平接口,直接赋值)
  502. const action: AgentAction = {
  503. type: parsed.action.type,
  504. content: parsed.action.content,
  505. target: parsed.action.target,
  506. visibility: parsed.action.visibility,
  507. motivation: parsed.motivation,
  508. plot_push: parsed.plotPush,
  509. }
  510. return {
  511. type: "agent-action",
  512. agent: cloneAgent(agent),
  513. action,
  514. round,
  515. node,
  516. stateChanges,
  517. timestamp: tlEvent.timestamp,
  518. timelineEvent: tlEvent,
  519. }
  520. }
  521. // ── 内部辅助:为旧 SimulationEvent 生成行为描述 ──
  522. function formatActionDescription(action: AgentAction, agentName: string): string {
  523. const { type, content, target } = action
  524. switch (type) {
  525. case "speak":
  526. return target
  527. ? `${agentName} 对 ${target} 说:「${content}」`
  528. : `${agentName} 说:「${content}」`
  529. case "evaluate":
  530. return `${agentName} 心中评价:${content}`
  531. case "pushPlot":
  532. return `${agentName} 采取行动:${content}`
  533. case "observe":
  534. return `${agentName} 观察到:${content}`
  535. case "react":
  536. return target
  537. ? `${agentName} 对 ${target} 的反应:${content}`
  538. : `${agentName} 做出反应:${content}`
  539. case "ally":
  540. return target
  541. ? `${agentName} 向 ${target} 示好结盟:${content}`
  542. : `${agentName} 寻求合作:${content}`
  543. case "confront":
  544. return target
  545. ? `${agentName} 与 ${target} 对抗:${content}`
  546. : `${agentName} 采取对抗姿态:${content}`
  547. case "conceal":
  548. return `${agentName} 隐瞒了内心想法:${content}`
  549. case "investigate":
  550. return `${agentName} 调查:${content}`
  551. case "act":
  552. return `${agentName} 行动:${content}`
  553. case "decide":
  554. return `${agentName} 做出决定:${content}`
  555. case "conflict":
  556. return target
  557. ? `${agentName} 与 ${target} 发生冲突:${content}`
  558. : `${agentName} 冲突:${content}`
  559. case "cooperate":
  560. return target
  561. ? `${agentName} 与 ${target} 合作:${content}`
  562. : `${agentName} 合作:${content}`
  563. case "withhold":
  564. return `${agentName} 隐瞒信息:${content}`
  565. default:
  566. return `${agentName}:${content}`
  567. }
  568. }
  569. // ── 内部辅助:检查节点是否达成目标(简单启发式) ──
  570. function isNodeGoalReached(
  571. _node: StoryNode,
  572. nodeTimelineEvents: TimelineEvent[],
  573. maxRounds: number,
  574. currentRound: number,
  575. ): boolean {
  576. // 达到最大轮次
  577. if (currentRound >= maxRounds - 1) {
  578. return true
  579. }
  580. // 启发式:产生了 pushPlot 类型的关键事件(剧情推动行为)
  581. const pushPlotCount = nodeTimelineEvents.filter(
  582. (e) => e.actionType === "pushPlot",
  583. ).length
  584. if (pushPlotCount >= 2) {
  585. return true
  586. }
  587. // 启发式:节点内事件数足够多(>=6个事件表示有足够互动)
  588. if (nodeTimelineEvents.length >= 6) {
  589. return true
  590. }
  591. return false
  592. }
  593. // ── 内部辅助:单个 Agent 决策并产生事件 ──
  594. async function agentDecideAndAct(
  595. agent: NovelAgent,
  596. node: StoryNode,
  597. state: SimulationState,
  598. llmConfig: LlmConfig,
  599. extraction: ExtractionResult,
  600. recentEventDescs: string[],
  601. injectionEvent: string | undefined,
  602. signal?: AbortSignal,
  603. ): Promise<{ parsed: ParsedAction; tlEvent: TimelineEvent; simEvent: SimulationEvent } | null> {
  604. // 1. 观察:筛选该 Agent 可见的时间线事件
  605. const visibleEvents = getVisibleEvents(
  606. agent.characterId,
  607. state.timelineEvents,
  608. 10,
  609. )
  610. // 2. 构建上下文(基于认知边界)
  611. const context = buildAgentContext(
  612. agent,
  613. node,
  614. recentEventDescs.slice(-8),
  615. extraction.worldRules,
  616. visibleEvents,
  617. )
  618. // 3. 构建 LLM 消息
  619. const messages: ChatMessage[] = [
  620. { role: "system", content: buildSystemPrompt(agent) },
  621. {
  622. role: "user",
  623. content: buildUserMessage(context, node, agent.name, injectionEvent),
  624. },
  625. ]
  626. // 4. 调用 LLM
  627. const rawResponse = await collectStream(llmConfig, messages, signal)
  628. if (signal?.aborted) return null
  629. // 5. 解析行为
  630. const parsed = parseAgentAction(rawResponse)
  631. // 6. 解析目标
  632. const target = resolveTarget(parsed.action.target, state.activeAgents)
  633. // 7. 创建时间线事件
  634. const tlEvent = createTimelineEvent(
  635. agent,
  636. parsed,
  637. target,
  638. state.currentRound,
  639. node.index,
  640. state.activeAgents,
  641. )
  642. // 8. 应用事件到相关 Agent 的记忆
  643. for (const id of tlEvent.observableBy) {
  644. const observer = state.activeAgents.get(id)
  645. if (observer) {
  646. applyEventToMemory(observer, tlEvent)
  647. }
  648. }
  649. // 9. 写入状态
  650. state.timelineEvents.push(tlEvent)
  651. // 10. 转换为旧 SimulationEvent
  652. const simEvent = timelineEventToSimulationEvent(
  653. tlEvent,
  654. agent,
  655. parsed,
  656. node,
  657. state.currentRound,
  658. )
  659. return { parsed, tlEvent, simEvent }
  660. }
  661. // ── 主入口:运行仿真(多智能体,基于认知边界) ──
  662. export async function runSimulation(
  663. input: SimulationInput,
  664. extraction: ExtractionResult,
  665. callbacks: SimulationCallbacks,
  666. signal?: AbortSignal,
  667. ): Promise<SimulationEvent[]> {
  668. const events: SimulationEvent[] = []
  669. const { agents, framework, wordBudget, llmConfig, injectionEvent } = input
  670. const totalNodes = framework.nodes.length
  671. const maxRounds = Math.max(
  672. 1,
  673. calcMaxRoundsPerNode(wordBudget) || MAX_ROUNDS_PER_NODE_FALLBACK,
  674. )
  675. let aborted = false
  676. // 初始化仿真状态
  677. const state: SimulationState = {
  678. currentRound: 0,
  679. timelineEvents: [],
  680. activeAgents: cloneAgentsToMap(agents),
  681. worldState: {},
  682. }
  683. try {
  684. for (let ni = 0; ni < totalNodes; ni++) {
  685. if (signal?.aborted) {
  686. aborted = true
  687. break
  688. }
  689. const node = framework.nodes[ni]
  690. // 确定参与角色:从 involvedCharacters(角色名)过滤 agents
  691. const nodeAgentIds = new Set<string>()
  692. for (const a of agents) {
  693. if (node.involvedCharacters.includes(a.name)) {
  694. nodeAgentIds.add(a.characterId)
  695. }
  696. }
  697. // 防御:若过滤结果为空,使用全部 agents
  698. let nodeAgentList: NovelAgent[]
  699. if (nodeAgentIds.size === 0) {
  700. nodeAgentList = Array.from(state.activeAgents.values())
  701. } else {
  702. nodeAgentList = Array.from(state.activeAgents.values()).filter((a) =>
  703. nodeAgentIds.has(a.characterId),
  704. )
  705. }
  706. // 设置本轮活跃 Agent
  707. const activeMap = new Map<string, NovelAgent>()
  708. for (const a of nodeAgentList) {
  709. activeMap.set(a.characterId, a)
  710. }
  711. state.activeAgents = activeMap
  712. // 产出 node-start 事件
  713. const startEvent: SimulationEvent = {
  714. type: "node-start",
  715. node,
  716. timestamp: new Date().toISOString(),
  717. }
  718. events.push(startEvent)
  719. callbacks.onEvent(startEvent)
  720. callbacks.onProgress(
  721. Math.round((ni / totalNodes) * 100),
  722. `开始节点 ${ni + 1}/${totalNodes}:${node.title}`,
  723. )
  724. // 当前节点内的事件描述(供 recentEvents 使用)
  725. const recentEventDescs: string[] = []
  726. const nodeTimelineEvents: TimelineEvent[] = []
  727. // 节点内多轮交互
  728. for (let round = 0; round < maxRounds; round++) {
  729. if (signal?.aborted) {
  730. aborted = true
  731. break
  732. }
  733. state.currentRound = round
  734. // a. 每个活跃 Agent 观察并决策
  735. for (const agent of nodeAgentList) {
  736. if (signal?.aborted) {
  737. aborted = true
  738. break
  739. }
  740. // 获取最新状态的 agent 引用(可能被之前的事件更新了记忆)
  741. const currentAgent = state.activeAgents.get(agent.characterId)
  742. if (!currentAgent) continue
  743. const result = await agentDecideAndAct(
  744. currentAgent,
  745. node,
  746. state,
  747. llmConfig,
  748. extraction,
  749. recentEventDescs,
  750. round === 0 ? injectionEvent : undefined,
  751. signal,
  752. )
  753. if (!result) {
  754. if (signal?.aborted) {
  755. aborted = true
  756. }
  757. break
  758. }
  759. const { parsed, tlEvent, simEvent } = result
  760. events.push(simEvent)
  761. callbacks.onEvent(simEvent)
  762. callbacks.onTimelineEvent?.(tlEvent)
  763. recentEventDescs.push(
  764. formatActionDescription(parsed.action, currentAgent.name),
  765. )
  766. nodeTimelineEvents.push(tlEvent)
  767. // d. 如果行为有目标且目标是可见的,触发目标的 react(反应链,限制深度)
  768. if (
  769. tlEvent.targetId &&
  770. parsed.action.type !== "react" &&
  771. parsed.visibility !== "self"
  772. ) {
  773. const targetAgent = state.activeAgents.get(tlEvent.targetId)
  774. if (targetAgent && REACT_CHAIN_LIMIT > 0) {
  775. await triggerReaction(
  776. targetAgent,
  777. currentAgent,
  778. tlEvent,
  779. node,
  780. state,
  781. llmConfig,
  782. extraction,
  783. recentEventDescs,
  784. events,
  785. callbacks,
  786. nodeTimelineEvents,
  787. signal,
  788. )
  789. }
  790. }
  791. }
  792. if (aborted) break
  793. // e. 检查节点目标是否达成
  794. if (isNodeGoalReached(node, nodeTimelineEvents, maxRounds, round)) {
  795. break
  796. }
  797. }
  798. if (aborted) break
  799. // 产出 node-complete 事件
  800. const completeEvent: SimulationEvent = {
  801. type: "node-complete",
  802. node,
  803. timestamp: new Date().toISOString(),
  804. }
  805. events.push(completeEvent)
  806. callbacks.onEvent(completeEvent)
  807. callbacks.onProgress(
  808. Math.round(((ni + 1) / totalNodes) * 100),
  809. `完成节点 ${ni + 1}/${totalNodes}:${node.title}`,
  810. )
  811. }
  812. if (!aborted && !signal?.aborted) {
  813. callbacks.onComplete(events)
  814. }
  815. return events
  816. } catch (err) {
  817. const error = err instanceof Error ? err : new Error(String(err))
  818. callbacks.onError(error)
  819. throw error
  820. }
  821. }
  822. // ── 内部辅助:触发目标 Agent 对事件的反应(react 行为) ──
  823. async function triggerReaction(
  824. targetAgent: NovelAgent,
  825. actor: NovelAgent,
  826. triggerEvent: TimelineEvent,
  827. node: StoryNode,
  828. state: SimulationState,
  829. llmConfig: LlmConfig,
  830. extraction: ExtractionResult,
  831. recentEventDescs: string[],
  832. events: SimulationEvent[],
  833. callbacks: SimulationCallbacks,
  834. nodeTimelineEvents: TimelineEvent[],
  835. signal?: AbortSignal,
  836. ): Promise<void> {
  837. if (signal?.aborted) return
  838. // 构建反应专用上下文:强调对刚才事件的反应
  839. const visibleEvents = getVisibleEvents(
  840. targetAgent.characterId,
  841. state.timelineEvents,
  842. 10,
  843. )
  844. const reactionNote = `\n\n【刚才发生的事情】\n${actor.name}刚刚对你做出了行为:[${triggerEvent.actionType}] ${triggerEvent.content}\n请你立即对此做出反应(react类型行为)。`
  845. const baseContext = buildAgentContext(
  846. targetAgent,
  847. node,
  848. recentEventDescs.slice(-8),
  849. extraction.worldRules,
  850. visibleEvents,
  851. )
  852. const context = baseContext + reactionNote
  853. const messages: ChatMessage[] = [
  854. { role: "system", content: buildSystemPrompt(targetAgent) },
  855. {
  856. role: "user",
  857. content:
  858. context +
  859. `\n\n请以「${targetAgent.name}」的视角,对${actor.name}刚才的行为立即做出反应,输出JSON。`,
  860. },
  861. ]
  862. try {
  863. const rawResponse = await collectStream(llmConfig, messages, signal)
  864. if (signal?.aborted) return
  865. const parsed = parseAgentAction(rawResponse)
  866. // 强制行为类型为 react
  867. parsed.action.type = "react"
  868. parsed.action.target = actor.name
  869. const tlEvent = createTimelineEvent(
  870. targetAgent,
  871. parsed,
  872. actor,
  873. state.currentRound,
  874. node.index,
  875. state.activeAgents,
  876. )
  877. // 应用记忆
  878. for (const id of tlEvent.observableBy) {
  879. const observer = state.activeAgents.get(id)
  880. if (observer) {
  881. applyEventToMemory(observer, tlEvent)
  882. }
  883. }
  884. state.timelineEvents.push(tlEvent)
  885. nodeTimelineEvents.push(tlEvent)
  886. const simEvent = timelineEventToSimulationEvent(
  887. tlEvent,
  888. targetAgent,
  889. parsed,
  890. node,
  891. state.currentRound,
  892. )
  893. events.push(simEvent)
  894. callbacks.onEvent(simEvent)
  895. callbacks.onTimelineEvent?.(tlEvent)
  896. recentEventDescs.push(
  897. formatActionDescription(parsed.action, targetAgent.name),
  898. )
  899. } catch {
  900. // 反应失败不中断主流程
  901. }
  902. }