
读长篇小说的体验里最扫兴的往往不是文笔而是“看了后面忘前面”。人物几十个伏笔埋了十几章时间线来回穿插等剧情真正解密时读者已经记不清前面的暗示。“溯阅”这类鸿蒙应用就是把这个问题拆成了产品方案导入本地小说用 AI 助手自动生成前情提要、人物关系、时间线伏笔同时强调数据本地优先不上传云端。对开发者来说这个功能背后不是简单接入一个模型而是一条完整的端侧文本处理链路文件导入、编码解析、章节切分、实体抽取、摘要生成、结构化存储最后还要在“本地优先”的约束下把隐私和性能都做好。这篇文章从工程实现角度拆解这条链路。适合正在做鸿蒙应用、想接入端侧 AI 能力、或者对“本地小说 阅读辅助”类产品感兴趣的开发者阅读。下面会先梳理功能和技术选型再按模块给出环境、代码、数据结构、模型集成、隐私处理和排查路径最后给出生产环境下的注意事项和扩展方向。1. 先拆解“本地优先 AI 阅读助手”的功能链路1.1 这类应用到底解决了什么问题长篇小说尤其是玄幻、历史、悬疑类大部头阅读负担往往来自三处人物数量多读者容易混淆同名、别名和不同势力关系。时间线跨度大过去章节埋下的线索被后续剧情重新提起时已经记不清原文。伏笔分散作者会把关键信息拆成很多小节放在不同章回里读者很难在需要时把它们串起来。“溯阅”这类应用的定位就是让 AI 在本地完成对小说文本的分析输出三类阅读辅助信息前情提要、人物关系、时间线伏笔。这三类信息不是简单搜索而是需要理解上下文之后生成的“阶段性总结”。实际产品里还可能包括当前阅读位置之前的剧情摘要、某个角色的出场经历、某条伏笔最早出现的章节等。从产品功能看它很像一个“个性化阅读助手”但从技术实现看它更接近一个“端侧文本分析管线”。理解这一点很重要因为开发核心不是“调一个模型”而是把文本处理、抽取、生成、存储串联起来。1.2 从导入到输出的完整技术链路以“导入一本本地 TXT 小说生成前情提要”为例完整链路可以拆成 7 个环节文件导入用户通过系统文件选择器选中本地 TXT 或 EPUB 文件。文件复制与沙箱存储把外部文件复制到应用专属目录避免直接持有用户目录的长期访问权限。编码识别与文本清洗识别 GBK、UTF-8 等编码去掉广告段落、空行、异常字符。章节切分与结构提取按“第 x 章”“序章”“卷名”等规则切分形成目录结构。内容索引与切片把长文本按段落或句子切成带位置信息的小块方便后续模型输入和阅读定位。信息抽取与 AI 生成先做规则抽取再用端侧模型生成摘要、人物关系、时间线伏笔。结构化存储与展示把结果写入本地数据库通过页面展示给用户。这条链路的前半段是通用文本处理后半段才真正进入 AI 环节。很多项目容易把顺序搞反一上来就接模型结果没有可靠的分章和切片数据模型生成的内容也无法对应到具体章节用户想回看原文时找不到位置。1.3 为什么“本地优先”会改变技术选型“数据本地优先”不只是一个隐私宣传点它会直接影响技术选型。如果所有处理都在端侧完成那么就不能沿用“上传全文到云端大模型 API”的方案而需要在本地完成存储、推理和缓存的整套设计。对比一下两种模式会更清楚技术维度云端处理本地优先小说全文需要上传到服务端只存在应用沙箱内模型能力可使用大参数模型效果强必须选择可落地端侧的小模型网络依赖依赖网络离线不可用完全离线可用隐私风险全文内容交给第三方服务默认不离开设备开发成本服务端和后端链路成本高需要优化模型、存储和推理性能可维护性模型升级由服务端控制模型文件随应用包分发或按需下载本地优先不是没有代价。它要求开发者在“模型效果”和“设备性能”之间做取舍也要在“功能丰富度”和“安装包体积”之间做取舍。比如一个 500MB 的模型虽然效果可能更好但会让应用下载成本变高也会在低端设备上出现内存不足的问题。很多鸿蒙应用会选择把核心模型控制在几十 MB 到一两百 MB同时把小说分章处理避免一次性把整本书喂给模型。2. 环境准备与工程结构设计2.1 鸿蒙开发环境的基本要求开发鸿蒙应用通常会用到官方 IDE 和 SDK。开始前至少需要确认以下环境是否具备DevEco Studio 已安装并且能创建 HarmonyOS 工程。本地 SDK 版本与目标设备系统版本匹配。准备真机或模拟器用于验证文件选择、数据库、模型推理等能力。如果要在端侧跑模型确认目标设备支持对应的推理框架和算子。具体版本号变化较快落地前要以官方文档和当前 SDK 为准不要直接照搬旧教程里的版本号。创建工程时一般选择 “Empty Ability” 模板即可。后续的关键能力由模块和依赖补充模板本身不需要太多特殊配置。2.2 项目模块划分“导入小说 AI 分析”不是一个写在单个页面里的功能。建议把工程按职责拆成多个目录或模块避免后续模型升级和页面迭代互相影响。一个参考结构如下entry/src/main/ets/ ├── entryability/ │ └── EntryAbility.ets ├── pages/ │ ├── Index.ets // 书架页 │ ├── ReaderPage.ets // 阅读页 │ └── AnalysisPage.ets // 前情提要/人物关系/时间线展示页 ├── common/ │ ├── constants/ │ │ └── RegexConstants.ets // 章节、清洗规则 │ └── utils/ │ ├── FileUtils.ets // 文件选择与复制 │ └── TextCleaner.ets // 文本清洗 ├── data/ │ ├── database/ │ │ ├── DatabaseHelper.ets // 数据库初始化 │ │ ├── BookDao.ets // 书籍表操作 │ │ ├── ChapterDao.ets // 章节表操作 │ │ └── AnalysisDao.ets // 分析结果表操作 │ └── models/ │ ├── Book.ets │ ├── Chapter.ets │ ├── Character.ets │ └── TimelineEvent.ets ├── ai/ │ ├── TextSegmenter.ets // 长文本切片 │ ├── RuleExtractor.ets // 规则抽取 │ ├── PromptBuilder.ets // 提示词构造 │ └── LocalModel.ets // 端侧模型推理封装 └── viewmodel/ └── BookViewModel.ets // 页面数据聚合这个分层的目的很简单文件处理、数据持久化、AI 推理、页面展示各管一段。以后想换模型、换数据库、加云同步都不需要改页面代码。2.3 最小依赖说明鸿蒙工程里许多能力通过 Kit 提供不需要引入大量第三方包。下面是最小依赖方向文件选择与操作系统文件选择器、文件 IO通常由kit.CoreFileKit提供。本地数据库kit.ArkData下的关系型数据库适合存书籍、章节、人物关系。端侧推理如果使用 MindSpore Lite 或其他推理框架需要按官方文档接入对应的 SDK。数据加密数据库加密、密钥存储优先使用系统安全能力。第三方框架的使用要特别谨慎因为鸿蒙生态的 API 仍在快速演进很多教程里的写法换一个 SDK 版本就不能编译。工程里的依赖尽量保持精简尤其是 AI 推理层建议封装成独立模块方便替换实现。注意本章节列出的 Kit 名称和 API 写法用于说明实现思路不同 SDK 版本的具体名称、导入路径和调用方式可能不同实际开发要以官方文档为准。3. 本地小说导入与文本解析3.1 文件选择与应用沙箱用户选择本地小说时系统通常通过文件选择器返回一个可读的 URI 或文件路径。应用不应该长期持有这个路径更不应该把所有文件操作都面向用户目录执行。推荐做法是用户授权后把选中的文件复制到应用沙箱目录后续解析、索引、模型分析都基于沙箱内的副本。下面是示意代码说明使用文件选择器和复制文件的基本流程import { picker } from kit.CoreFileKit; import { fileIo } from kit.CoreFileKit; async function importNovelToSandbox(): Promisestring { const documentPicker new picker.DocumentViewPicker(); const selectResult await documentPicker.select({ maxSelectNumber: 1, }); if (!selectResult || selectResult.length 0) { return ; } const sourceFile selectResult[0]; // 沙箱路径实际应用应使用 context 获取 filesDir const sandboxDir getContext().filesDir; const destPath ${sandboxDir}/novel_${Date.now()}.txt; const file fileIo.openSync(sourceFile, fileIo.OpenMode.READ_ONLY); const outFile fileIo.openSync(destPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); // 使用流式读写避免一次性加载大文件 fileIo.copyFileSync(file.fd, outFile.fd); fileIo.closeSync(file); fileIo.closeSync(outFile); return destPath; }这段代码的关键点是只把文件复制到沙箱后续所有读取都从destPath进行。这样即使用户 Source 路径发生变化也不会影响应用内部的数据完整性。3.2 编码识别与文本清洗中文小说 TXT 文件最常见的编码是 UTF-8 和 GBK。如果按固定编码读取很容易出现乱码。稳妥的做法是读取前几百字节做编码探测再用探测结果解码如果探测失败可以尝试按 UTF-8 解码遇到非法字节再回退到 GBK 或 GB18030。下面是一个清洗函数的方向function cleanText(raw: string): string { return raw // 移除广告行“最新章节”这类常见插入文本 .replace(/^\s*(最新|最新章节|请收藏|推荐).*$/gm, ) // 移除多余空行 .replace(/\n{3,}/g, \n\n) // 移除控制字符 .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ) .trim(); }需要注意清洗不能太激进。章节标题、对话中的换行、作者的话等都要尽量保留否则后续按章节切分或人物关系抽取时会丢失重要上下文。3.3 章节切分与全书结构章节切分是后续所有分析的基础。如果章节切错了前情提要和人物关系就无法准确对应原文。常见章节标题格式包括第一章 出发第1章 出发序章楔子第一卷 风起Chapter 1(1) 夜行可以用正则做初步匹配再对部分异常标题做白名单处理。示例正则const CHAPTER_REGEX /^\s*(第[0-9一二三四五六七八九十百千零两][章节回卷部篇]|序章|楔子|番外|Chapter\s*\d)\s*[^\n]{0,30}$/i;切分流程可以按行扫描遇到标题行时开启新章节累积内容直到下一个标题。大文本建议按行处理而不是一次性 split 成数组避免内存峰值过高。切分完成后每一章需要记录章节序号、标题、起始字符偏移和结束字符偏移。偏移量用于后续跳转原文和生成“该伏笔最早出现在第 x 章”的定位。3.4 数据模型与建表 SQL关系型数据库适合存储这种结构清晰的书籍数据。以下是简化版建表语句CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, source_path TEXT NOT NULL, author TEXT DEFAULT , total_chapters INTEGER DEFAULT 0, created_at INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS chapters ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, chapter_index INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, start_offset INTEGER DEFAULT 0, end_offset INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_chapters_book ON chapters(book_id, chapter_index); CREATE TABLE IF NOT EXISTS characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, name TEXT NOT NULL, aliases TEXT DEFAULT [], first_chapter_index INTEGER DEFAULT 0, description TEXT DEFAULT ); CREATE TABLE IF NOT EXISTS character_relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, character_a TEXT NOT NULL, character_b TEXT NOT NULL, relation_type TEXT DEFAULT , evidence TEXT DEFAULT , mention_count INTEGER DEFAULT 0 );数据库初始化在鸿蒙端使用kit.ArkData的relationalStore代码方向如下import { relationalStore } from kit.ArkData; const config: relationalStore.StoreConfig { name: book_ai.db, securityLevel: relationalStore.SecurityLevel.S1, }; const store await relationalStore.getRdbStore(getContext(), config); await store.executeSql(CREATE_TABLE_SQL);这里的SecurityLevel.S1表示低级别安全等级适合普通小说阅读数据。如果后续加入用户笔记、隐私信息需要提高安全等级或增加加密配置。4. 前情提要、人物关系和时间线的数据基础4.1 从“全文切片”到“可分析语料”小说文本很长端侧模型一次能处理的上下文有限。为保证模型输入有效需要先把全文切成若干切片。每个切片要保留原始位置信息方便后续结果回溯。推荐切片粒度是“章节 - 段落块”每个块控制在 500 到 1000 字。示例 JSON 结构如下[ { bookId: 1, chapterIndex: 12, chapterTitle: 迷雾, blockIndex: 3, text: 她推开旧宅大门时看见墙上的画换了一幅……, startOffset: 30211, endOffset: 30812 } ]切片不只是为了给模型输入也是为了规则抽取时知道某个角色是在哪一章出现的。比如第一次出现的章节索引越小说明这个角色越可能是核心人物。4.2 先做规则抽取再用模型兜底很多人会直接让模型输出“所有人物关系”但长篇小说人物多、上下文复杂模型一次输出可能不够准还可能遗漏。更实用的做法是先用规则抽取候选再用模型进行确认和补充。规则抽取可以做三件事候选人物识别通过人物称谓词表、书名号、引号内称呼、连续出现的人名模式收集候选实体。共现关系计算统计两个候选人物在同一个章节或同一个段落块中共同出现的次数。共现次数高说明可能存在关系。时间词定位找“第二天”“三日后”“十年前”“深夜”“傍晚”这类时间标记结合章节顺序建立时间线候选。下面是一个简单的共现关系统计逻辑function countCooccurrences(blocks: TextBlock[], charNames: string[]): Mapstring, number { const pairCount new Mapstring, number(); for (const block of blocks) { const matched charNames.filter((name) block.text.includes(name)); for (let i 0; i matched.length; i) { for (let j i 1; j matched.length; j) { const key ${matched[i]}|${matched[j]}; pairCount.set(key, (pairCount.get(key) ?? 0) 1); } } } return pairCount; }这段代码体现的是“同现”的思路如果两个人物频繁在同一章出现则他们可能有直接关系。它不能代替模型判断关系类型但能有效缩小模型需要分析的范围。4.3 关系与事件的存储结构规则抽取的结果和模型生成的结果可以统一保存。人物关系表需要包含证据章节方便用户点击关系时回看原文。时间线伏笔则建议单独建表CREATE TABLE IF NOT EXISTS timeline_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, chapter_index INTEGER NOT NULL, time_hint TEXT DEFAULT , event_desc TEXT DEFAULT , related_characters TEXT DEFAULT [], is_foreshadowing INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS foreshadowing_markers ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, keyword TEXT NOT NULL, first_chapter_index INTEGER NOT NULL, last_chapter_index INTEGER DEFAULT 0, note TEXT DEFAULT );时间线伏笔的“伏笔”部分比普通时间线更依赖模型判断。规则可以识别“重复关键词”比如某个物品、某句话在多个章节反复出现但“它是不是伏笔”需要模型结合剧情上下文判断。所以设计表结构时建议把确定性高的规则结果和模型判断结果分开存储或者加一个source字段标记来源。这样可以避免模型生成内容污染规则数据。用户查看时可以同时看到“规则识别出的重复意象”和“模型判断的潜在伏笔”两者并不冲突。5. 接入端侧模型生成摘要与分析结果5.1 端侧模型选型的关键指标鸿蒙应用里跑 AI 模型不能只看“效果最好”的大模型。要考虑安装包体积、内存占用、推理速度、模型格式和算子兼容性。以下指标在选型时最需要关注指标说明对小说阅读应用的影响模型体积模型文件大小影响安装包大小和下载时间过大的模型不适合随应用发布Token 上限一次能处理的文本长度决定是把整章直接送入还是需要先切片推理耗时生成一次结果需要的时间影响用户体验超过 10 秒需要做进度提示中文效果对中文长文本的理解能力直接影响摘要和人物关系的可读性算子支持是否兼容当前设备 NPU/CPU不支持会导致推理失败或极慢根据设备性能不同可能需要准备多档模型低端机使用更小的模型生成简短摘要高端机使用稍大模型生成更细致的人物关系。模型文件不建议直接打进安装包首次使用时在用户同意后按需下载可以降低入门门槛。5.2 文本分段与上下文窗口处理小说的“前情提要”需要覆盖很长的剧情但模型输入不能无限长。常见处理方法是分层摘要第一层把章节切分为多个文本块每个块生成“块级摘要”。第二层把块级摘要按章节拼接生成“章节摘要”。第三层把最近的若干章节摘要拼接生成“前情提要”。这种方案的好处是每层输入都在模型上下文限制内坏处是摘要过程可能丢失细节。为减少丢失可以在切片时尽量保留关键实体名、时间词、对话中的转折点。示例伪代码async function generateBookSummary(bookId: number): Promisestring { const chapters await chapterDao.getChapters(bookId); const chapterSummaries: string[] []; for (const chapter of chapters) { const blocks splitChapterIntoBlocks(chapter.content, 800); const blockSummaries: string[] []; for (const block of blocks) { const prompt PromptBuilder.blockSummary(block.text); const result await localModel.generate(prompt); blockSummaries.push(result); } const chapterPrompt PromptBuilder.chapterSummary( chapter.title, blockSummaries.join(\n) ); chapterSummaries.push(await localModel.generate(chapterPrompt)); } // 最后一批章节生成前情提要 const recent chapterSummaries.slice(-20); return await localModel.generate(PromptBuilder.recentSummary(recent.join(\n))); }这个流程让每一次模型调用都在可控范围内同时保证最终摘要覆盖的是最近 N 个章节的核心信息。对于“看后面忘前面”的阅读场景最近 20 至 50 章的摘要通常最有价值。5.3 提示词模板设计端侧模型通常指令跟随能力不如云端大模型因此提示词要更具体、更结构化。下面提供三个方向的模板参考。前情提要你是一个小说阅读助手。请根据下面的小说片段输出前情提要。 要求 1. 只概括片段中的重要情节。 2. 按故事发生顺序输出不要加入你的猜测。 3. 使用中文不超过200字。 4. 如果有伏笔或未解悬念在最后用“伏笔”开头单独列出一句。 片段 {chunk_text}人物关系请从下面小说文本中找出人物之间的关系。 输出格式为一行一条 人物A | 人物B | 关系类型 | 理由 关系类型只使用亲属、师徒、朋友、敌对、同事、恋人、其他。 如果无法判断不要输出。 文本 {chunk_text}时间线伏笔请从下面小说文本中提取时间线事件和伏笔。 对每个事件输出 时间事件发生的时间提示 事件发生了什么 相关人物人物名用逗号分隔 是否伏笔是或否 文本 {chunk_text}提示词模板要放进单独模块而不是散落在业务代码里。这样后续可以根据不同模型调整提示词而不影响其他逻辑。5.4 在鸿蒙端的推理流程以 MindSpore Lite 这类端侧推理框架为例鸿蒙端加载和执行模型的基本流程是确认模型文件已经放在应用可访问目录。初始化推理环境。创建输入张量把文本编码后的数据填入。执行推理获取输出张量。把输出张量解码为文本。示意代码如下import { mindSporeLite } from kit.MindSporeLiteKit; async function runInference(prompt: string): Promisestring { const modelPath ${getContext().filesDir}/models/reader_summary.mindir; const model await mindSporeLite.loadModelFromFile({ path: modelPath }); const inputTensor model.createTensor({ shape: [1, 512], dataType: int32 }); const tokenIds tokenize(prompt, 512); inputTensor.setData(tokenIds); const outputs await model.predict([inputTensor]); const outputText decodeToText(outputs[0].getData()); model.free(); return outputText; }这里的tokenize和decodeToText需要根据实际模型分词器实现。不同模型的分词器差异很大这一步不能省略也不建议直接使用模型内部分词器绕过去。端侧推理最容易被忽略的是输入长度对齐和分词器一致性问题模型训练时用什么分词器推理时就要用同样的分词规则。注意如果本机没有对应算子或模型格式与推理框架版本不兼容会在loadModelFromFile阶段直接报错。真机调试前应先用一个最小输入样本验证模型能正常加载和输出。6. 数据本地优先与隐私保护落地6.1 应用沙箱与数据隔离“数据本地优先”不是简单地把文件放在本地而是要让数据只有在用户主动操作时才离开设备。落地时需要注意几层隔离外部文件被复制进沙箱之前原路径不受应用控制应用也不应长期监听该路径。沙箱内的小说副本、解析结果、AI 生成内容都应放在应用私有目录。不要轻易使用公共存储目录保存分析结果否则其他应用可能读取到。鸿蒙应用可以通过context.filesDir、context.cacheDir等目录区分长期数据和缓存。小说正文属于长期数据应放在 filesDir章节切分的临时切片可以放 cacheDir清理缓存时不影响用户书籍信息。6.2 数据库加密与敏感信息保护小说本身可能不算高敏感数据但用户阅读记录、笔记、AI 生成的分析结果往往反映个人偏好。为了稳妥数据库可以启用加密能力密钥存放在系统安全存储中而不是硬编码在代码里。具体加密方式会随 SDK 版本变化设计时应提前确认当前数据库是否支持加密以及加密对查询性能的影响。加密后数据库文件即使被导出也无法直接读取。6.3 权限最小化优先使用系统文件选择器而不是申请“所有文件访问权限”。这样用户可以选择某个文件却不会把整个存储目录暴露给应用。AI 推理如果需要从网上下载模型也需要在用户授权后进行并明确告知模型用途和数据上传情况。一个合理的权限策略是不申请不必要的存储权限。不默认申请网络权限。模型下载需要用户点击“下载离线模型包”后触发。不把小说内容和模型请求混杂在一起如果模型本身需要在云端推理产品上要明确提示“内容将被发送到云端”。6.4 数据导出、备份与删除本地优先不代表数据永远存在。应用应提供可理解的数据管理入口“导出分析结果”把人物关系、时间线生成 JSON 或 Markdown让用户保存到其他位置。“删除书籍”同时删除书籍正文、章节、分析结果和 AI 缓存。“清空应用数据”一次性从设置页移除所有沙箱数据。删除操作要写清楚影响范围避免用户误删重要笔记后无法恢复。如果后续加入了云备份功能删除本地数据时还需要询问是否同步删除云端副本。6.5 本地优先与云同步的取舍对“溯阅”这类产品本地优先最大的优势是隐私和离线体验。但纯本地也会带来两个问题换设备后数据无法跟随。端侧模型效果受设备限制。如果未来要支持多设备同步建议把“同步”和“分析”拆开用户主动开启云同步后只上传加密后的分析结果或阅读进度不默认上传小说全文。这样既保留隐私又能让数据跨设备流转。技术实现上可以先给每条记录加sync_status字段后续再接云同步服务而不是一开始就设计成“全文必须上传”。7. 运行验证、性能优化与生产环境差异7.1 用一本样章跑通完整流程开发阶段不建议一上来就拿整本几百万字的小说测试。先准备一本 3 到 5 章、每章 3000 字左右的样章按以下步骤验证用文件选择器导入 TXT确认沙箱目录出现副本。检查清洗后的文本确认没有乱码和广告残留。检查章节切分结果确认章节标题和章节数量正确。运行规则抽取确认人物候选和共现关系数据落库。对第一章生成前情提要确认输出内容与原文一致。验证人物关系、时间线伏笔的页面展示。预期输出示例前情提要 少年林昭在青石镇破庙中救下重伤的少女苏念并从她身上找到半块玉佩。 当日夜里镇上出现黑衣人在搜寻苏念的下落。林昭带着苏念离开小镇 准备前往剑宗寻找她的师叔。苏念没有告诉林昭玉佩的另一半在哪 这是目前最大的悬念。 伏笔玉佩的另一半与林昭的身世有关。人物关系示例林昭 | 苏念 | 朋友 | 在破庙中救下苏念并同行前往剑宗 苏念 | 黑衣人 | 敌对 | 黑衣人正在搜寻苏念 林昭 | 老乞丐 | 师徒 | 老乞丐教过林昭基础拳法7.2 正确性验证方法AI 生成内容的“正确性”不能用简单的断言判断。建议建立一个小型评测集比如选 10 章文本人工标注出关键人物和伏笔然后对比模型输出与标注的重合度。人物关系的评估看“准确率”和“召回率”前情提要的评估可以看“关键情节是否被覆盖”和“是否有虚构内容”。对端侧应用来说还要关注每次生成的稳定性。同一个片段在不同设备上不应该有剧烈差异。如果模型输出随机性太强可以考虑固定随机种子或使用温度参数较低的生成配置。7.3 性能瓶颈与优化策略长篇小说在端侧处理性能瓶颈通常集中在三处瓶颈现象优化方向大文件解析导入后卡顿页面无响应改用流式解析放到异步线程分批处理章节模型推理耗时生成摘要需要十几秒使用小模型、批量预生成、后台任务、进度提示内存占用模型加载后 App 被系统回收控制并发推理推理完成立即释放模型避免加载多个模型一个常用的优化是“预生成”在用户阅读到第 20 章时后台提前生成第 21 到 30 章的前情提要缓存。这样用户翻到后面时内容已经从缓存读取不需要等待模型现场推理。另一个优化是“模型复用”。不要每个页面都重新加载一次模型。把模型实例保存在单例或专门的管理器中页面进入时只创建推理会话离开时释放资源。7.4 学习环境与生产环境的差异学习环境可以跑通即可生产环境还多一套工程要求配置外置模型路径、提示词版本、是否需要下载模型包等要放到配置文件中。日志记录每次推理的耗时、输入长度、是否失败但不要记录小说全文只记录采样片段或字符数。监控统计模型加载失败率、推理失败率、数据库异常率。回滚模型文件更新后如果新模型效果明显变差用户应该能切换回旧模型。这些不是一两天能全部做完的但设计阶段就要留出位置。否则一旦线上用户规模上来再改数据表结构或模型调用方式会非常痛苦。8. 常见问题排查8.1 导入与解析阶段问题现象可能原因检查方式处理建议导入 TXT 后全屏乱码编码识别失败按 UTF-8 解码了 GBK 文件查看文件头字节确认目标编码增加编码探测失败时回退 GBK/GB18030章节数量明显偏少章节标题格式不匹配正则没覆盖打印切分结果看漏掉哪些标题扩充正则加入“第x节”“番外”等模式文本里有广告残留清洗规则太弱输出清洗前后的差异样本增加“本章未完”“最新网址”等广告特征词大文件导入卡死一次性读取全文到内存观察内存和主线程状态改为流式读取异步解析8.2 AI 推理阶段问题现象可能原因检查方式处理建议模型加载失败模型格式与推理框架版本不兼容查看启动日志中的具体异常码重新导出模型或升级推理框架推理结果为空输入文本被分词器截断或 token 超限打印 token 数量和输入长度缩小文本块重新对齐输入尺寸生成内容与原文无关提示词不清晰或输入块跨章节用最小片段单独调用模型按章节块输入提示词中强调“只能基于给定文本”推理时 App 闪退内存不足模型太大或多模型同时加载查看崩溃日志监控内存占用使用更小模型推理前释放无用资源8.3 数据与隐私相关问题现象可能原因检查方式处理建议删除书籍后磁盘未释放只删数据库记录没删沙箱文件检查沙箱目录大小删除书籍时同步清理正文和缓存切片数据库文件被其他工具打开没有启用加密或加密等级不匹配检查数据库安全等级配置启用数据库加密密钥放系统安全存储模型下载后重新启动丢失模型放在 cacheDir被系统清理检查文件目录生命周期模型应放在 filesDir 或专门管理的目录中排查时建议先按“输入是否正确 - 路径是否存在 - 权限是否缺少 - 版本是否兼容 - 日志是否有异常”的顺序走不要一上来就改模型或换算法。很多问题其实出在文件编码和路径拼接上。9. 最佳实践与扩展方向9.1 可复用的发布前检查清单如果准备把这类应用发布到生产环境建议逐项检查以下内容文件导入支持 TXT、EPUB 时编码处理是否覆盖 GBK、UTF-8、UTF-16。数据库是否存在外键级联删除删除书籍时关联表是否自动清理。模型文件是否带版本号提示词模板是否与模型版本强绑定。是否提供“下载模型包”的明确入口和失败重试逻辑。是否记录推理耗时和失败率方便后续优化。是否在隐私说明中明确“本地优先”的范围以及哪些场景可能访问网络。是否在页面展示 AI 结果的“可能不准确”提示避免用户把模型输出当作原文。是否已用不同尺寸真机测试低内存设备是否会出现闪退。9.2 扩展方向“前情提要、人物关系、时间线伏笔”是一个很好的核心场景技术上还可以向这些方向扩展阅读结束后的“全书总结”按卷生成剧情梗概。按角色生成“人物传记式”摘要点击角色查看所有相关情节。阅读中实时悬浮提示当前段落提到某个旧伏笔自动弹出去相关章节。多格式导入支持 EPUB 解析和章节元数据读取提升结构化质量。把分析结果导出为 Markdown 或 JSON方便用户二次整理。这些方向不会改变核心架构只是在上层增加更多“阅读理解”功能。9.3 对新手的练习建议如果刚开始接触鸿蒙端侧 AI 阅读助手不建议直接做完整产品。可以先分三步练习先写一个能导入 TXT、按章节切分、展示目录的阅读器把文本处理和数据库基础打牢。再接入一个已有的端侧摘要模型只对单个章节生成摘要验证模型加载、推理和输出展示。最后再加入人物关系和时间线抽取先用规则实现再逐步用模型增强。这三步走完之后再考虑书架、阅读进度、多本书管理等产品化功能。阅读类应用的体验核心是“能把内容讲清楚”AI 只是放大器。如果章节切分和原文定位做不好再强的模型也无法让用户信任。回到开头那个问题读大部头小说为什么容易忘因为人类记忆需要线索。一个真正可用的本地 AI 阅读助手本质是在帮用户构建一条可回查、可跳转、可理解的线索链。这个目标听起来很 AI但实现它的地基其实是文件解析、数据建模、端侧推理和隐私保护这些非常工程化的事情。对开发者来说先把地基修稳再谈模型效果才是这类应用最长久的做法。