小说Agent计划(八): 章节写作 (Chapter Writing)
小说Agent计划(八): 章节写作 (Chapter Writing)
October 3, 2025
与 Blog 同步开发的开源项目: PlotWeave
本文的内容是完成小说 Agent 计划(四)-章节写作 (Chapter Writing)的具体实现。
设计
Agent 流程设计
首先,回顾一下章节写作的设计,即之前提出的创作循环:
flowchart TD A[阅读需求] --> B[向世界记忆请求信息] B --> C[请求更改] C --> D{监督是否认为更改合理?} D -->|是| E[获取更改链并根据更改链输出文字内容] D -->|否| F[分析失败原因并再次检索信息] F --> C
对比之前两步的聊天式 agent,这是一个完全不同的流程。无法直接复用之前的 langgraph
流程图并且只是简单的替换工具集。需要重新设计一个新的 langgraph
流程图。
flowchart TD direction TB Start[入口: 设置初始State<br/>任务: PLANNING] --> Agent{Agent 节点<br/>根据当前任务执行} Agent --> Router{路由节点<br/>检查State和最新消息} Router -- "需要工具? PLANNING阶段" --> Tools[工具节点] Tools --> Agent Router -- "信息足够,开始提议" --> ProposeChanges[设置State<br/>任务: PROPOSING_CHANGES] ProposeChanges --> Agent Router -- "更改已提议" --> Supervisor[监督节点<br/>人机交互] Supervisor -- "批准" --> Approve[设置State<br/>任务: WRITING] Approve --> Agent Supervisor -- "拒绝" --> Reject[设置State<br/>任务: REVISING] Reject --> Agent Router -- "写作完成" --> Finish(完成)
Agent 实现
首先需要定义上面流程图中提到的状态:
class WritingState(str, Enum):
PLANNING = "计划"
PROPOSING_CHANGES = "开始提议"
REVIEW = "审查"
WRITING = "写作"
COMPLETE = "完成"
然后就像 agent.py
中定义聊天式 agent 一样,定义流程图状态。
class State(TypedDict):
"""
写作agent的状态
"""
messages: list[BaseMessage]
"""
会话历史
"""
project_id: UUID
"""
当前项目ID
"""
writing_state: WritingState
"""
当前写作状态
"""
current_chapter_index: int
"""
当前章节索引
"""
current_chapter_info: ChapterInfo
"""
当前章节信息
"""
world: World
"""
世界记忆图谱
"""
然后定义 llm 节点
llm = init_chat_model(
model=config.writer_model,
model_provider="openai",
base_url=config.writer_base_url,
api_key=config.writer_api_key,
)
llm_with_tools = llm.bind_tools( # pyright: ignore[reportUnknownMemberType]
world_tools.read_and_append_tools
+ writer_tools.full_tools
+ [switch_writing_state_tool]
)
# build_hint_prompt就是提示词构建函数,较长,这里不展示
def writer_bot(state: State):
"""
llm节点
"""
messages = state["messages"]
last_message = messages[-1]
# 注入当前的工作状态
if isinstance(last_message.content, str): # pyright: ignore[reportUnknownMemberType]
last_message.content = (
f"--- 当前模式的提示 ---\n\n{build_hint_prompt(state['writing_state'], state['current_chapter_info'])}\n\n--- 当前模式的提示 ---"
+ last_message.content
)
return {"messages": [llm_with_tools.invoke(state["messages"])]}
然后是监督节点,这里暂时占位,因为我还没想好具体怎么实现人机交互。
def review_node(
state: State,
):
"""
监督节点
"""
# TODO: 实现监督逻辑,现在直接通过
return {"writing_state": WritingState.WRITING}
最后使用一个路由边来控制流程。
def router_edge(
state: State,
):
"""
路由节点
"""
messages = state["messages"]
last_msg = messages[-1]
writing_state = state["writing_state"]
if isinstance(last_msg, AIMessage) and getattr(last_msg, "tool_calls", []):
return "tools"
else:
match writing_state:
case WritingState.COMPLETE:
return END
case WritingState.REVIEW:
return "review"
case _:
return "writer_bot"
最后,构建流程图并编译图。
graph_builder.add_node("writer_bot", writer_bot) # pyright: ignore[reportUnknownMemberType]
graph_builder.add_edge(START, "writer_bot")
tool_node = ToolNode(tools=tools)
graph_builder.add_node("tools", tool_node) # pyright: ignore[reportUnknownMemberType]
graph_builder.add_conditional_edges(
"writer_bot",
router_edge,
# The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node
# It defaults to the identity function, but if you
# want to use a node named something else apart from "tools",
# You can update the value of the dictionary to something else
# e.g., "tools": "my_tools"
{"tools": "tools", "review": "review", END: END},
)
graph = graph_builder.compile() # pyright: ignore[reportUnknownMemberType]
至此,agent 流程的主要实现就完成了,剩下的就是监督节点的实现和前端交互的实现了。
后端接口
不同于之前的两个阶段,小说章节写作阶段是一个比较长的流程,因此需要设计一个新的后端接口来支持这个流程。
首先需要一个可持久化的标志记录写到了哪个章节,可以保存在 metadata。
class ProjectMetadata(BaseModel):
"""
小说项目的元数据
包括以下内容
- name: 项目名称
- phase: 当前阶段
- id: 项目唯一标识符
- writing_chapter_index: 当前正在写作的章节索引,默认为 0,表示第一章
"""
name: str = "未命名项目"
phase: ProjectPhase
id: str
writing_chapter_index: int = 0
然后修改写作 agent 的导航边,让它在章节写作完成后,更新项目的 writing_chapter_index
。
def router_edge(
state: State,
):
"""
路由边
"""
messages = state["messages"]
last_msg = messages[-1]
writing_state = state["writing_state"]
if isinstance(last_msg, AIMessage) and getattr(last_msg, "tool_calls", []):
return "tools"
else:
match writing_state:
case WritingState.COMPLETE:
state["metadata"].writing_chapter_index += 1 # here
return END
case WritingState.REVIEW:
return "review"
case _:
return "writer_bot"
最后,定义后端接口。
class WritingRequest(BaseModel):
"""
启动写作任务的请求体
"""
chapter_index: int
chapter_info: ChapterInfo
async def run_writing_agent_in_background(
project_id: str, chapter_index: int, chapter_info: ChapterInfo
):
"""
在后台执行写作 Agent 的 langgraph astream,并将事件放入对应的队列。
"""
# 获取此任务专用的队列
queue = writing_event_queues.get(project_id)
if not queue:
print(f"错误:项目 {project_id} 的事件队列未找到。")
return
try:
project_instance = await active_projects.get(project_id)
# 初始化 Agent 状态
init_message = "你是一个专业的小说作家,正在按照预设的流程创作小说段落。"
state = writer_agent.State(
messages=[SystemMessage(content=init_message)],
project_id=uuid.UUID(project_id),
writing_state=writer_agent.WritingState.PLANNING,
current_chapter_index=chapter_index,
current_chapter_info=chapter_info,
world=project_instance.world,
metadata=project_instance.metadata,
approved_events=[],
)
async for event in writer_agent.graph.astream( # pyright: ignore[reportUnknownMemberType]
state, config={"recursion_limit": 1145141919810}
):
for _, value_update in event.items():
if "messages" in value_update:
latest_message = value_update["messages"][-1]
stream_data = None
if isinstance(latest_message, AIMessage):
if latest_message.tool_calls:
tool_name = latest_message.tool_calls[0]["name"]
stream_data = {
"type": "thinking",
"data": f"正在调用工具: `{tool_name}`...",
}
elif latest_message.content: # type: ignore
stream_data = { # type: ignore
"type": "content_chunk",
"data": latest_message.content, # type: ignore
}
elif isinstance(latest_message, ToolMessage):
stream_data = {
"type": "tool_result",
"data": f"工具 `{latest_message.name}` 返回: {latest_message.content}", # type: ignore
}
if stream_data:
await queue.put(stream_data) # type: ignore
except Exception as e:
error_data = {"type": "error", "data": f"写作 Agent 执行出错: {str(e)}"}
await queue.put(error_data) # type: ignore
raise e
finally:
print(f"写作任务流结束: 项目 {project_id}, 章节索引 {chapter_index}")
# 发送结束信号
end_data = {"type": "end", "data": "写作任务流结束"}
await queue.put(end_data) # type: ignore
# 放入一个 None 来告诉 stream_writing_progress 停止
await queue.put(None)
@app.post("/api/projects/{project_id}/write/start", status_code=202)
async def start_writing_chapter(project_id: str, request: WritingRequest):
"""
为指定项目启动一个章节写作任务。
"""
# 如果已有任务在运行,则不允许启动新任务
if project_id in writing_tasks and not writing_tasks[project_id].done():
raise HTTPException(
status_code=409, detail="该项目已有一个正在进行的写作任务。"
)
# 为这个任务创建一个新的事件队列
writing_event_queues[project_id] = asyncio.Queue()
# 在后台创建一个 Task 来运行 Agent
task = asyncio.create_task(
run_writing_agent_in_background(
project_id, request.chapter_index, request.chapter_info
)
)
writing_tasks[project_id] = task
print(f"接收到项目 {project_id} 的写作请求,目标章节索引: {request.chapter_index}")
return {"message": "写作任务已成功启动"}
@app.get("/api/projects/{project_id}/write/stream")
async def stream_writing_progress(project_id: str):
"""
连接并流式传输指定项目写作任务的进度。
"""
async def event_generator():
"""从队列中获取事件并格式化为 SSE"""
queue = writing_event_queues.get(project_id)
if not queue:
# 如果队列不存在,说明任务可能还未启动或已结束
error_data = {"type": "error", "data": "未找到写作任务流。请先启动任务。"}
yield f"data: {json.dumps(error_data)}\n\n"
return
try:
while True:
# 从队列中等待一个事件
event = await queue.get()
if event is None:
# None 是结束信号
break
yield f"data: {json.dumps(event)}\n\n"
except asyncio.CancelledError:
# 当客户端断开连接时,会触发此异常
print(f"客户端断开连接,停止为项目 {project_id} 发送事件。")
finally:
# 清理资源
if project_id in writing_tasks:
del writing_tasks[project_id]
if project_id in writing_event_queues:
del writing_event_queues[project_id]
return StreamingResponse(event_generator(), media_type="text/event-stream")
@app.get("/api/projects/{project_id}/write/current_chapter_index", response_model=int)
async def get_current_writing_chapter_index(project_id: str):
"""
获取当前正在写作的章节索引。
"""
instant = await active_projects.get(project_id)
return instant.metadata.writing_chapter_index
前端实现
前端的类似之前的分章页面,但是需要显示写作结果和写作日志
import { useState, useEffect, useCallback, useRef } from "react";
import { useParams } from "react-router-dom";
import {
List,
BookOpen,
Sparkles,
Save,
Loader2,
Circle,
Pencil,
Workflow,
BrainCircuit,
TerminalSquare,
AlertTriangle,
Lock, // 引入 Lock 图标
} from "lucide-react";
import { Button } from "../components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "../components/ui/card";
import { ScrollArea } from "../components/ui/scroll-area";
import { Textarea } from "../components/ui/textarea";
import { type ProjectMetadata, ProjectPhase } from "../components/ProjectCard";
import { streamAsyncIterator } from "../lib/utils";
import type { ChapterInfo } from "@/lib/types";
// 为此页面定义一个更完整的章节类型
interface ChapterWritingInfo {
title: string;
intent: string;
content: string;
status: "empty" | "draft";
}
// 写作过程中的日志类型
interface WritingLog {
id: string;
type: "thinking" | "tool_result" | "error";
data: string;
}
// 新的日志条目卡片组件
const LogEntryCard = ({ log }: { log: WritingLog }) => {
let IconComponent;
let title;
let cardClasses;
let iconClasses;
switch (log.type) {
case "thinking":
IconComponent = BrainCircuit;
title = "思考中...";
cardClasses =
"bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-700";
iconClasses = "text-blue-500";
break;
case "tool_result":
IconComponent = TerminalSquare;
title = "工具调用结果";
cardClasses =
"bg-green-50 border-green-200 dark:bg-green-900/30 dark:border-green-700";
iconClasses = "text-green-600";
break;
case "error":
IconComponent = AlertTriangle;
title = "发生错误";
cardClasses =
"bg-red-50 border-red-300 dark:bg-red-900/30 dark:border-red-700";
iconClasses = "text-red-600";
break;
default:
IconComponent = Workflow;
title = "日志";
cardClasses = "bg-muted/50";
iconClasses = "text-muted-foreground";
}
return (
<div className={`p-3 rounded-lg border flex flex-col gap-2 ${cardClasses}`}>
<div className="flex items-center gap-2">
<IconComponent className={`size-4 flex-shrink-0 ${iconClasses}`} />
<p className={`text-xs font-semibold ${iconClasses}`}>{title}</p>
</div>
<p className="text-xs font-mono text-foreground/80 whitespace-pre-wrap break-words">
{log.data}
</p>
</div>
);
};
function ChapterWritingPage() {
const { projectId } = useParams<{ projectId: string }>();
const [chapters, setChapters] = useState<ChapterWritingInfo[]>([]);
const [selectedChapterIndex, setSelectedChapterIndex] = useState<
number | null
>(null);
const [currentWritableIndex, setCurrentWritableIndex] = useState<
number | null
>(null); // 新增状态:当前可写作的章节索引
const [currentContent, setCurrentContent] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [writingLogs, setWritingLogs] = useState<WritingLog[]>([]);
const [project, setProject] = useState<ProjectMetadata | null>(null);
const [error, setError] = useState<string | null>(null);
const logsEndRef = useRef<HTMLDivElement>(null);
const isReadOnly = project?.phase !== ProjectPhase.CHAPER_WRITING;
const fetchProjectData = useCallback(async () => {
if (!projectId) return;
try {
const response = await fetch(`/api/projects/${projectId}`);
if (!response.ok) throw new Error("获取项目信息失败");
const data = await response.json();
setProject(data);
} catch (e) {
if (e instanceof Error) setError(e.message);
}
}, [projectId]);
// 新增函数:获取当前可写作的章节索引
const fetchWritableIndex = useCallback(async () => {
if (!projectId) return;
try {
const response = await fetch(
`/api/projects/${projectId}/write/current_chapter_index`
);
if (!response.ok) throw new Error("获取当前写作章节索引失败");
const data = await response.json();
setCurrentWritableIndex(data);
} catch (e) {
if (e instanceof Error) setError(e.message);
}
}, [projectId]);
const fetchChapters = useCallback(
async (selectFirst = false) => {
if (!projectId) return;
try {
const response = await fetch(`/api/projects/${projectId}/chapters`);
if (!response.ok)
throw new Error(`获取章节列表失败: ${response.statusText}`);
const data = await response.json();
const chapterInfos: ChapterWritingInfo[] = await Promise.all(
(data.chapters || []).map(
async (chap: ChapterInfo, index: number) => {
try {
const contentRes = await fetch(
`/api/projects/${projectId}/chapters/${index}`
);
if (contentRes.ok) {
const contentText = await contentRes.text();
return {
title: chap.title,
intent: chap.intent,
content: contentText,
status: contentText ? "draft" : "empty",
};
}
return {
title: chap.title,
intent: chap.intent,
content: "",
status: "empty",
};
} catch (error) {
console.error(`获取章节 ${index} 内容失败:`, error);
return {
title: chap.title,
intent: chap.intent,
content: "",
status: "empty",
};
}
}
)
);
setChapters(chapterInfos);
if (selectFirst && chapterInfos.length > 0) {
// 默认选中第一个或当前可写的章节
setSelectedChapterIndex(currentWritableIndex ?? 0);
}
} catch (e) {
if (e instanceof Error) setError(e.message);
}
},
[projectId, currentWritableIndex]
);
useEffect(() => {
fetchProjectData();
fetchWritableIndex(); // 同时获取可写章节索引
fetchChapters(true);
}, [fetchProjectData, fetchChapters, fetchWritableIndex]);
useEffect(() => {
const selected =
selectedChapterIndex !== null ? chapters[selectedChapterIndex] : null;
setCurrentContent(selected?.content || "");
}, [selectedChapterIndex, chapters]);
useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [writingLogs]); // 依赖项是 writingLogs,每次日志更新时触发
const getStatusIcon = (
status: ChapterWritingInfo["status"],
index: number
) => {
// 判断章节是否被锁定
const isLocked =
currentWritableIndex !== null && index > currentWritableIndex;
if (isLocked) {
return <Lock className="size-4 text-muted-foreground flex-shrink-0" />;
}
switch (status) {
case "draft":
return <Pencil className="size-4 text-yellow-500 flex-shrink-0" />;
case "empty":
default:
return (
<Circle className="size-4 text-muted-foreground flex-shrink-0" />
);
}
};
const handleGenerate = async () => {
if (
selectedChapterIndex === null ||
!projectId ||
!chapters[selectedChapterIndex]
)
return;
setIsGenerating(true);
setCurrentContent("");
setWritingLogs([]);
setError(null);
try {
const chapter_info = {
title: chapters[selectedChapterIndex].title,
intent: chapters[selectedChapterIndex].intent,
};
const startResponse = await fetch(
`/api/projects/${projectId}/write/start`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chapter_index: selectedChapterIndex,
chapter_info,
}),
}
);
if (startResponse.status !== 202) {
const errorData = await startResponse.json();
throw new Error(
`启动写作任务失败: ${errorData.detail || "Unknown error"}`
);
}
const progressResponse = await fetch(
`/api/projects/${projectId}/write/stream`
);
if (!progressResponse.ok || !progressResponse.body) {
throw new Error("连接到进度流失败");
}
for await (const chunk of streamAsyncIterator(progressResponse.body)) {
if (chunk) {
try {
const event = JSON.parse(chunk);
switch (event.type) {
case "content_chunk":
setCurrentContent((prev) => prev + event.data);
break;
case "thinking":
case "tool_result":
case "error":
setWritingLogs((prev) => [
...prev,
{ id: `log-${Date.now()}-${Math.random()}`, ...event },
]);
break;
case "end":
console.log("写作任务流结束");
break;
}
} catch (e) {
console.error("SSE 解析错误:", e);
}
}
}
} catch (err) {
if (err instanceof Error) setError(err.message);
} finally {
setIsGenerating(false);
await fetchChapters(); // 重新获取章节状态
await fetchWritableIndex(); // 重新获取可写索引,可能会解锁下一章
}
};
const handleSave = async () => {
if (selectedChapterIndex === null || !projectId || isSaving) return;
setIsSaving(true);
setError(null);
try {
const response = await fetch(
`/api/projects/${projectId}/chapters/${selectedChapterIndex}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: currentContent }),
}
);
if (!response.ok) throw new Error(`保存失败: ${response.statusText}`);
setChapters((prev) =>
prev.map((chap, index) =>
index === selectedChapterIndex
? {
...chap,
content: currentContent,
status: currentContent ? "draft" : "empty",
}
: chap
)
);
} catch (err) {
if (err instanceof Error) setError(err.message);
} finally {
setIsSaving(false);
// 保存草稿不一定解锁下一章,但可以同步一下状态
await fetchWritableIndex();
}
};
const selectedChapter =
selectedChapterIndex !== null ? chapters[selectedChapterIndex] : null;
// 判断当前选中的章节是否被锁定
const isCurrentChapterLocked =
selectedChapterIndex !== null &&
currentWritableIndex !== null &&
selectedChapterIndex > currentWritableIndex;
return (
<div className="flex flex-col h-full gap-4">
<div>
<h1 className="text-3xl font-bold">章节写作</h1>
<p className="mt-2 text-muted-foreground">
{isReadOnly ? "(只读模式)" : "将你的创意转化为生动的文字。"}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 flex-1 min-h-0">
<Card className="lg:col-span-1 flex flex-col min-h-0">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<List className="size-5" /> 章节导航
</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-hidden p-2">
<ScrollArea className="h-full">
<div className="space-y-2 pr-2">
{chapters.map((chap, index) => {
const isLocked =
currentWritableIndex !== null &&
index > currentWritableIndex;
return (
<Button
key={index}
variant={
selectedChapterIndex === index ? "secondary" : "ghost"
}
className="w-full justify-start h-auto text-left"
onClick={() => setSelectedChapterIndex(index)}
disabled={isLocked} // 禁用锁定的章节
title={isLocked ? "请先完成前面的章节" : ""}
>
<div className="flex items-start gap-3 p-1">
{getStatusIcon(chap.status, index)}
<div>
<p className="font-semibold">{chap.title}</p>
<p className="text-xs text-muted-foreground font-normal whitespace-normal">
{chap.intent}
</p>
</div>
</div>
</Button>
);
})}
</div>
</ScrollArea>
</CardContent>
</Card>
<Card className="lg:col-span-2 flex flex-col min-h-0">
{selectedChapter ? (
<>
<CardHeader>
<CardTitle>{selectedChapter.title}</CardTitle>
<CardDescription>{selectedChapter.intent}</CardDescription>
<div className="flex items-center gap-2 pt-2">
<Button
onClick={handleGenerate}
disabled={
isGenerating ||
isSaving ||
isReadOnly ||
selectedChapterIndex !== currentWritableIndex // 只能生成当前可写的章节
}
>
{isGenerating ? (
<Loader2 className="size-4 mr-2 animate-spin" />
) : (
<Sparkles className="size-4 mr-2" />
)}
{isGenerating
? "生成中..."
: selectedChapter.status !== "empty"
? "重新生成"
: "生成内容"}
</Button>
<Button
onClick={handleSave}
variant="outline"
disabled={
isGenerating ||
isSaving ||
isReadOnly ||
isCurrentChapterLocked // 不能保存未解锁的章节
}
>
{isSaving ? (
<Loader2 className="size-4 mr-2 animate-spin" />
) : (
<Save className="size-4 mr-2" />
)}
保存
</Button>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col gap-4 overflow-hidden p-4">
{/* 文本编辑区 */}
<div className="flex-1 flex flex-col min-h-0">
<ScrollArea className="h-full w-full rounded-md border">
<Textarea
className="flex-1 resize-none w-full h-full p-4 border-0 focus-visible:ring-0"
value={currentContent}
onChange={(e) => setCurrentContent(e.target.value)}
placeholder={
isGenerating
? "写作 Agent 正在奋笔疾书..."
: "点击“生成内容”开始创作,或在此处手动输入..."
}
disabled={
isGenerating || isReadOnly || isCurrentChapterLocked // 不能编辑未解锁的章节
}
/>
</ScrollArea>
</div>
{/* Agent 日志区 */}
<p className="text-sm font-semibold pb-2 flex items-center gap-2">
<Workflow className="size-4" /> Agent 工作日志
</p>
<div className="flex-1 flex flex-col min-h-0">
<ScrollArea className="h-full flex-1 bg-muted/50 rounded-md p-4">
{writingLogs.length > 0 ? (
<div className="space-y-3">
{writingLogs.map((log) => (
<LogEntryCard key={log.id} log={log} />
))}
<div ref={logsEndRef} />
</div>
) : (
<div className="text-center text-muted-foreground text-sm py-4 h-full flex items-center justify-center">
{isGenerating
? "等待 Agent 日志..."
: "此处将显示写作过程中的 Agent 思考和工具调用日志。"}
</div>
)}
</ScrollArea>
</div>
</CardContent>
</>
) : (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<BookOpen className="size-12 mb-4" />
<p>
{chapters.length > 0
? "请从左侧选择一个章节开始写作"
: "请先在“分章”阶段创建章节"}
</p>
</div>
)}
</Card>
</div>
</div>
);
}
export default ChapterWritingPage;
测试
下面是尝试测试的文段结果
# 血色倒计时
深夜的研究室里,林辰的眉头紧锁,指尖在键盘上快速敲击。桌面上散落着血色玫瑰连环杀人案的卷宗,受害者都是年轻女性,现场无一例外地留下了诡异的血色玫瑰。作为顶尖犯罪心理侧写师,他试图从这些冰冷的文字和照片中,捕捉到那个隐藏在阴影中的杀手的心跳。
就在他全神贯注地分析着案件特征时,手机屏幕突然亮起。不是来电,也不是消息,而是一个血红色的倒计时数字——60。林辰震惊地拿起手机,试图关闭这个诡异的画面,但无论他如何操作,屏幕都纹丝不动,那刺目的血色数字无情地跳动着:59、58、57……
心跳随着倒计时的节奏加速,林辰的警觉性提到了最高点。这个超自然现象完全超出了他的理解范围——一个无法关闭的倒计时,一个强制占据屏幕的血色数字。他环顾四周,研究室里只有案件卷宗和电脑屏幕的微光,一切都显得如此正常,唯独这个倒计时在提醒他,某种未知的力量正在介入他的世界。
当倒计时归零的瞬间,整个世界仿佛被按下了暂停键。研究室的光线扭曲、消散,林辰感到一阵天旋地转的眩晕。当他重新站稳时,发现自己已经身处一个无边无际的纯白空间——这里没有任何物体,没有任何声音,只有永恒的白色延伸至视野的尽头。
就在他试图理解这个纯白空间的本质时,一道冰冷的声音在脑海中响起:「神选游戏系统已激活。玩家林辰,欢迎进入初始空间。」随着系统的提示,一股前所未有的力量在他体内涌动,仿佛某种沉睡的天赋被唤醒了。他的思维变得前所未有的清晰,对犯罪心理的洞察力瞬间提升到了一个新的层次。
「天赋【心理侧写LV.1】已觉醒。」系统的声音再次响起。林辰闭上眼睛,感受着这个新能力带来的变化。他发现自己能够更深入地理解犯罪者的心理动机,能够从更细微的线索中还原犯罪现场的全貌。这个天赋似乎是他专业能力的升华,但又不完全相同——它带着某种超自然的特质。
站在这个纯白的初始空间中,林辰意识到自己的人生已经发生了根本性的改变。从血色倒计时的出现,到被强制拉入这个神秘空间,再到系统的激活和天赋的觉醒——这一切都指向一个事实:他已经被选中,成为了这个名为"神选游戏"的一部分。而那个他正在调查的血色玫瑰连环杀人案,或许与这一切有着某种深刻的联系。
他深吸一口气,感受着体内涌动的力量。心理侧写LV.1——这个天赋或许正是他追查血色玫瑰连环杀人案的关键。在这个全新的起点上,林辰知道,他不仅要面对现实世界中的罪犯,还要在这个神秘的系统中找到自己的位置。血色倒计时已经结束,但真正的游戏才刚刚开始。
可以看到基本上可以工作,但是有些显得平淡和简单,字数也不多(大概 1000 字)。后续需要继续调优提示词等。
Last updated on