
1. 项目定位与核心痛点1.1 为什么需要「结构化记忆」这两年大模型写作已经不算新鲜事了市面上各种 AI 写作工具一大堆但真正用下来你会发现一个非常尴尬的问题模型本身没有记忆。而且这个没记忆还不是简单的上下文长度不够是更深层的结构性问题。我用某主流大模型接口做个人博客辅助写作的时候体验特别典型。上午让它帮我写一篇技术方案的开头下午再让它继续写后半段它已经把上午定的技术选型、术语定义、表达风格全部忘光了。就算你一口气写完一篇长文写到两三千字的时候前面埋下的伏笔、定义的缩写、列举的数据来源它统统开始含糊其辞。这不是模型笨而是无状态接口的固有缺陷——每次请求都是一次全新的对话模型只能依赖你塞给它的有限上下文来猜。Goink 这个项目要解决的就是这个问题。它的核心思路很直接在模型外部维护一套结构化的记忆系统把写作过程中产生的关键信息——人物设定、术语定义、章节大纲、数据引用、风格偏好——显式地存下来在下一次请求时自动检索并注入提示词。用一句话概括就是让 AI 写作从翻聊天记录找线索变成查自己的知识库再动笔。1.2 记忆应该长什么样先说一个我在最初设计时走过的弯路。我一开始以为所谓的记忆就是把聊天记录存下来下次拼接进 prompt 里实践下来发现完全不行。原因很简单聊天记录里有大量冗余信息寒暄、修正、口误、重复这些噪音全部塞进上下文既浪费 token又会干扰模型对当前任务的理解。真正的记忆应该是经过提炼的、结构化的、可检索的知识单元。Goink 把记忆设计成四类实体事实记忆写作中确定下来的客观信息比如产品名叫 Goink目标用户是独立开发者定价 299 元/年。决策记忆做过哪些选择为什么这么选比如后端用 Go 是因为部署简单单二进制无依赖。风格记忆用户的写作风格偏好比如正文段落要短每段不超过三行尽量不用被动语态。结构记忆文章的整体框架章节顺序、标题层级、线索伏笔比如第二章要埋一个性能对比数据第六章再回收。这四类记忆各有各的用途事实记忆保证一致性决策记忆保证逻辑连贯风格记忆保证文风统一结构记忆保证长文不散。四者合起来才构成一套完整的长文写作记忆系统。1.3 技术选型为什么是 Go React选型这部分我在做技术预研的时候其实对比过好几套方案。最初用 Python 写了一个原型FastAPI React跑通了基本流程但遇到两个问题第一Python 的服务部署到小内存服务器上动不动就吃掉三四百 MB 内存不够轻第二跑原型的时候要装一堆依赖环境稍微不对就起不来。后来换到 Go体验完全不一样。编译出来就是一个可执行的二进制文件扔到服务器上直接跑内存占用大概只有 Python 方案的五分之一。这对一个自部署的写作工具来说非常重要——很多用户是把自己的笔记服务跑在一台 1G 内存的小机器上的资源很金贵。前端选 React 而没有选 Vue主要是团队熟悉度问题加上 React 生态里的流式渲染方案比较成熟。Goink 前端要做的是边生成边显示的流式输出React 配合 SSE 用起来非常顺手。如果你对 Vue 更熟这个架构完全可以平移核心逻辑其实都是围绕后端提供流式接口、前端逐步渲染来设计的。2. 总体架构与数据流设计2.1 模块划分Goink 的架构不算复杂总共拆成六个模块每个模块职责单一边界清晰。模块职责关键技术点记忆仓库记忆实体的存取、更新、删除用 SQLite 持久化支持关键字段索引向量索引记忆的语义检索调外部嵌入接口生成向量存向量数据上下文引擎从记忆仓库中筛选并组装 prompt按相关性评分排序控制注入 token 数量写作服务编排一次完整的生成请求串联记忆检索、上下文组装、模型调用事件网关处理前端的长连接和流式推送SSEServer-Sent Events实现控制台前端用户交互界面React 单页应用提供编辑器与记忆面板模块之间通过内部接口通信没有搞微服务那套复杂的东西。原因很简单Goink 的部署场景绝大多数是单机自部署拆微服务只会增加运维负担。就我个人的经验来说单体应用把所有模块放在一个进程里代码组织好了完全够用。2.2 一次写作请求的完整数据流我把一次完整的写作请求拆开来看整个链路是这样的第一步前端发起生成请求。用户在编辑器的继续写作输入框里写下接着写第三章点击生成按钮前端会把这个指令连同当前文章的 ID 一起发给后端。第二步后端加载文章上下文。后端拿到文章 ID 后先从 SQLite 里加载文章的基本信息——标题、当前章节、总字数、写作进度。这些信息会作为基础上下文进入下一步。第三步检索相关记忆。这是整个系统最核心的一步。上下文引擎会将接着写第三章这个指令向量化去记忆仓库里做语义检索找到跟第三章主题相关的记忆条目。同时还会做一个结构化查询查一下第三章对应的大纲里有哪些要点、有哪些需要回收的伏笔。第四步组装提示词。把检索到的记忆按照系统提示 - 事实记忆 - 结构记忆 - 风格记忆 - 历史片段 - 用户指令的顺序拼装成一个完整的 prompt。这个顺序不是随便定的后面我会详细讲。第五步调用模型接口。写作服务把 prompt 发给大模型的 API使用流式模式接收返回结果。第六步边接收边落盘和推送。后端每收到一段增量文本就通过 SSE 推给前端渲染。同时把已经生成的文本追加到文章的草稿表里这样用户即使中途关闭页面已生成的内容也不会丢失。第七步异步更新记忆。等整段生成完成后后台会触发一次记忆提炼流程把新生成的内容里出现的实体、事实、决策点抽出来判断哪些是已有的、哪些是新增的、哪些是和已有记忆冲突的然后做相应的更新。2.3 为什么没有用长上下文方案有人可能会问现在大模型的上下文窗口已经很大了几十万 token直接把历史内容全塞进去不就行了为什么还要搞一套记忆系统这个问题我实际测试过。把全文都塞进上下文确实能解决一致性问题但会带来几个新麻烦一是成本问题。大模型 API 的计费是输入输出的 token 都算钱的一次请求塞进去几万 token虽然输出质量上去了但费用也成倍上涨。对于一个高频写作工具来说这个成本是不可接受的。二是干扰问题。我在测试中发现当上下文过长时模型会迷失在中间。就是它记得开头和结尾的内容但中间夹杂的大量无关信息会分散它的注意力导致关键指令反而执行不到位。结构化记忆的精髓是只注入对当前任务真正有用的信息而不是一股脑全塞进去。三是风格漂移的问题。长上下文中如果混入了多轮对话里的口语化片段模型的文风会不自觉地向这些片段靠拢。而提炼过的风格记忆是浓缩的、明确的反而能让模型更稳定地保持统一风格。所以 Goink 最终选择了一条中间路线不是不用历史信息而是先用提炼和检索将历史信息压缩到最精炼的状态再以小体积注入 prompt。效果上一致性接近全量注入成本却只有它的十分之一不到。3. 结构化记忆模型与存储设计3.1 记忆实体的底层设计记忆实体是 Goink 的核心数据结构我把它设计成一个通用型的条目字段上尽量精简但又足够灵活。记忆条目的核心字段包括ID全局唯一标识用自增整型保持简单。类别事实、决策、风格、结构四选一。内容纯文本形式的一句话或一小段描述。内容要求自包含也就是单独拿出来就能看懂不依赖额外上下文。标签用于结构化检索的标签数组比如第三章产品定价技术选型。标签可以重叠一条记忆可以打多个标签。来源文章这条记忆是从哪篇文章里提炼出来的。相关实体记忆涉及的实体关键词通常是名词比如GoinkSQLiteSSE。这个字段单独存是为了做关键词过滤。置信度0 到 1 之间的小数表示这条记忆的可信程度。置信度低于阈值的记忆不会被注入 prompt。创建时间与更新时间用于做时间衰减和遗忘策略。这个设计借鉴了我之前做推荐系统时的经验记忆可以理解为一种内容标签和用户的某个文章绑定但又具备跨文章复用的能力。比如用户写了两篇不同的文章都提到了Goink这个产品名如果两篇文章共用一套记忆仓库那新文章可以直接复用旧文章里对 Goink 的定义不用再重新交代一遍。3.2 记忆间的关联关系单条记忆存下来只是第一步真正让记忆系统有价值的是记忆之间的关联。Goink 用了两种关联方式第一种是标签关联。给记忆打上标签后标签本身就构成了一种隐式的聚簇。比如所有带技术选型标签的记忆在检索为什么选这个方案这个意图时可以一次性拉出来。这种关联实现简单但是检索结果比较粗糙——它只能匹配字面标签不能理解语义。第二种是引用关联。在提炼记忆时如果一条记忆的内容里提到了另一条记忆提到的实体就会自动建立一条引用关系。比如后端用 Go 是因为部署简单这条记忆提到了Go如果之前有一条Go 是编译型语言的记忆也提到了Go它们之间就会建立弱关联。在检索时命中其中一条就可以顺带把另一条作为关联记忆一并召回。这里有一个我在设计时特别在意的细节关联关系的建立不能急于求成。我原本想过用图数据库存记忆关联后来发现图数据库在这个场景下有点大材小用。记忆之间的关联不像社交图谱那样密集用关系表加索引完全可以支撑而且查询性能更好。图数据库的灵活查询能力在数据量没上去之前完全是冗余开销。3.3 存储层实现要点存储我用了 SQLite而不是 MySQL 或者 PostgreSQL。原因有三个第一Goink 自部署场景是单用户或小团队使用并发写入的量级很低SQLite 完全扛得住。第二SQLite 是单文件存储备份恢复极其简单把那个文件拷贝走就是一份完整备份。对于个人笔记工具来说这个特性太实用了。第三Go 对 SQLite 的驱动非常成熟编译时静态连接部署时不需要额外装数据库服务。不过 SQLite 有个天然的弱点是并发写。Goink 做了两层缓解方案。第一层所有写操作统一走一个串行化的写入队列避免并发写冲突。第二层写频繁的模块——比如流式生成时每接收一段文本就更新草稿——用内存缓冲批量落盘而不是每次增量都直接写数据库。这样在实测中即使是连续长时间写作SQLite 的写入也不会成为瓶颈。如果你想让 Goink 在更大的数据规模下运行存储层可以平滑替换成 PostgreSQL。这个迁移成本我已经控制在很小范围内因为所有的数据库操作都封装在记忆仓库模块内部上层应用不感知。4. 记忆检索与上下文组装4.1 双通道检索向量召回 结构化过滤记忆的检索是整个系统里最讲究的部分直接决定了生成质量。Goink 采用向量召回 结构化过滤的双通道模式。向量召回这一步做的事情是把用户当前输入比如继续写第三章的架构设计通过嵌入模型转成一个向量然后和记忆仓库里所有记忆的向量做相似度计算返回相似度最高的一批候选。但搜索引擎的经验告诉我一个道理纯向量检索有一个明显问题——它擅长捕捉语义接近但不擅长处理精确匹配。比如用户写的是第三章这个明确的指代如果靠向量检索可能会召回一堆跟架构相关的记忆而不会精确命中标记了第三章标签的那批记忆。所以还需要结构化过滤来配合。结构化过滤做的事情是从用户输入里提取出明确的实体和标签比如第三章架构设计模块拆分然后用这些关键词去数据库里做精确匹配把相关的记忆直接拎出来。这个通道的精度更高不受嵌入模型的语义理解能力限制。两个通道的结果会汇总进入下一步的排序和去重最终形成当前任务真正需要的记忆集合。4.2 提示词组装的优先级这一步是整个 Goink 系统对生成质量影响最大的环节——印象中很多做记忆系统的人都栽在这记忆检索出来了但是不会组装一股脑按检索顺序排进 prompt结果模型还是抓不住重点。Goink 的组装顺序是实践总结出来的大致如下系统提示System Prompt固定不变的全局指令比如你是一名写作助手请根据提供的素材进行写作保持风格一致。事实记忆和当前任务直接相关的不可变事实比如产品名、用户需求、技术选型。放这个位置是因为模型处理到中段时注意力最容易分散需要最硬核的信息打底。结构记忆当前章节的写作目标、大纲要点、需要回收的伏笔。让模型明确这一段要写什么、写到哪、为什么写。风格记忆用户的写作偏好。放在这是为了让模型在真正动笔之前先进入状态。历史片段之前几段已生成的内容摘要注意是摘要而不是全文。让模型衔接上文时有个抓手。用户指令用户当前的实际输入比如接着写第三章。这个顺序背后有个很朴素的认知逻辑先把客观事实交代清楚再交代任务目标再设定表达风格最后才给出具体指令。模型按照这个顺序接收信息生成的连贯性和符合度都更高。4.3 Token 预算与动态裁剪上下文组装还有一个容易被忽略的问题token 预算。每次请求能注入的记忆总量必然是有限的否则成本会失控。实践中我给 Goink 设定了一套动态裁剪机制先估算硬性上限。调用模型时要根据模型的最大上下文长度减去输出保留长度得出可用的输入预算。比如一个模型最大上下文是 8000 token我留 2000 token 给输出那输入预算就是 6000 token。再分配各项占比。预算是这样分配的系统提示约 200 token用户指令约 200 token历史片段约 800 token剩下的 4800 token 全部留给各类记忆。在记忆内部事实记忆优先供应结构记忆次之风格记忆再次相关度低的记忆最后裁掉。如果预算不够就做降级处理把低置信度的记忆先砍掉再砍向量相似度最低的记忆最后砍掉时间最早且不再被引用的旧记忆。这个裁剪逻辑是层层递进的不会出现砍掉关键事实保留下琐碎风格的问题。导航记忆时要注意一个反向陷阱不是所有对当前有用的信息都值得注入。比如用户正在写技术方案查询到了一条关于该产品的历史版本变化的记忆表面上很相关但这条记忆对当前任务的帮助其实有限。因为模型在写具体章节时不需要知道产品的完整演化史只需要知道当前这个版本的关键特性。所以裁剪时我会加入一个任务相关性的权重而不是单纯看检索相似度。4.4 这个设计模式的可复用性双通道检索加动态裁剪这套模式不止适用于写作场景。做客服机器人、做知识库问答、做代码辅助生成都是同一个模板先用结构化手段锁定精确目标再用语义手段扩展候选集最后用预算约束控制注入量。这个模式我在另一个项目里复用过一次帮企业搭建内部知识问答系统效果也很稳。原因在于它本质上解决的是如何在有限上下文里最大化利用后台知识库这个问题——这是所有 RAG 系统的共同痛点。5. Go 后端的关键实现细节5.1 接口设计与请求模型Goink 后端的接口设计遵循一个原则面向任务而不是面向资源。前端不关心后端怎么组织记忆只关心我现在要干什么。实际的路由设计大概是这样的POST /api/articles新建文章返回文章 ID。PUT /api/articles/{id}保存草稿全文。POST /api/articles/{id}/generate发起一次续写请求。请求体里只需要传用户指令后端自动会去检索记忆、组装 prompt、调用模型。GET /api/articles/{id}/stream建立 SSE 长连接接收生成过程中的流式文本。POST /api/articles/{id}/memories/extract手动触发一次记忆提炼。GET /api/memories?article_idcategorytag查询记忆列表供前端展示和编辑。这个接口设计有一个优势前端逻辑非常简单不需要理解内部的知识处理流程。用户点了生成按钮前端只需要发一个generate请求然后开一个stream连接收文本不知道也完全没问题。Go 这边用的是标准库net/http加轻量路由没有引入重量级框架。Go 1.22 之后的标准库路由已经支持了路径参数对小项目来说完全够用。越晚引入框架项目的依赖就越小长期维护的负担也越轻。5.2 流式响应为什么要用 SSE 而不是 WebSocket这里要解释一个选型问题。初版我试过用 WebSocket后来换成了 SSE原因有几点SSE 是建立在 HTTP 之上的单向推送协议实现简单只需要设置响应头Content-Type: text/event-stream然后在 Go 里循环往 ResponseWriter 写入数据即可。前端用原生的EventSource对象就能消费不需要额外的库。WebSocket 在 Go 里通常要引入第三方库比如某个 WebSocket 实现库而且它还需要处理连接握手、心跳、断线重连这一堆事情。在这个场景里数据流是单向的——只会从服务端往客户端流前端到后端就是发一个普通的 POST 请求。既然如此何必要上一个全双工协议呢SSE 还有一个隐藏的好处它是纯 HTTP 的中间可以套任何标准的 HTTP 代理和负载均衡不容易被特殊协议阻断。我在部署到 Nginx 后面时只需要加一句配置禁用代理缓冲SSE 就能顺利穿透。下面是一个用 Go 实现 SSE 的简化版func (s *Server) StreamWriter(w http.ResponseWriter, r *http.Request) { flusher, ok : w.(http.Flusher) if !ok { http.Error(w, streaming unsupported, http.StatusInternalServerError) return } w.Header().Set(Content-Type, text/event-stream) w.Header().Set(Cache-Control, no-cache) w.Header().Set(Connection, keep-alive) // writerSink 是生成过程中增量文本的通道 for chunk : range s.writerSink { // SSE 数据格式以 data: 开头空行结尾 fmt.Fprintf(w, data: %s\n\n, chunk) flusher.Flush() } }代码不复杂但有一个细节必须注意每条消息必须以两次换行结尾否则前端的 EventSource 无法正确解析。我第一次实现时漏了这一点结果前端收到的是一个永远不触发的 onmessage 事件排查了半个小时。5.3 并发控制与幂等处理生成请求有一个特别烦人的并发场景用户等得不耐烦连点了好几次生成按钮结果同时触发了三个生成任务每个任务都往同一篇文章里追加内容最后文章结构乱成一团。Goink 的解决方案是每篇文章同一时间只允许一个生成任务在跑。实现上用了 Go 的sync.Mutex加一个生成状态标志。当收到一个生成请求时先尝试获取文章的互斥锁如果锁被占用就返回错误已有生成任务正在进行中请稍候。另外还有一层幂等保护每个生成请求在发起时都会带一个client_request_id服务端把这个 ID 连同文章 ID 一起做成唯一索引。如果前端因为网络原因重试了同一个请求服务端能识别出来并返回已存在的结果不会重复生成。幂等处理是我在实际使用中受益很大的一个设计。有一次前端请求因为网络抖动发了两遍如果没有幂等保护文章就会出现两段重叠的内容而有了它重复请求会被稳妥地拦截掉。5.4 模型 API 调用的重试与降级模型 API 是全网中最不可控的一环。网络抖动、服务端超时、限流都有可能让生成中断。Goink 做了三层防护第一层请求超时控制。每个模型调用设置一个较长的上下文等待时间但如果超过了设定值就中断当前请求返回可读的错误信息。第二层自动重试。如果是网络错误或者服务端的 5xx 错误会自动重试一次间隔 2 秒。如果重试也失败就不再重试了避免产生不可控的重复计费。第三层模型降级。如果主模型连续失败可以自动切换到备用的模型接口虽然速度可能慢一些但至少能保证服务可用。降级切换对用户是透明的前端会收到一个提示告诉他当前使用的是备用模型。这三层防护下来我在实际使用中遇到过的异常情况基本都有兜底不会出现一次网络抖动就导致写作进度全部丢失的惨剧。6. React 前端的设计与实现6.1 交互布局的核心思路Goink 前端的整体布局采用两栏结构左边是写作区右边是记忆面板。这个布局不是随便定的是根据写作场景的实际需求来的。写作区是用户的主要工作区用了一个开源的 Markdown 编辑器组件支持实时预览、快捷键插入格式、字数统计。编辑器本身不是项目的重点所以我刻意没有自己去重新造轮子而是选了一个成熟组件快速集成。记忆面板的设计经历过一轮迭代。最初我把它做成一个只读列表用户只能看到模型自动提炼的记忆不能修改。但实际用下来发现自动提炼的结果经常有噪音——比如把一句普通的过渡句当成事实记下来。所以后来加了三个能力手动新增记忆、编辑记忆内容、删除记忆。这样用户可以对自动提炼的结果进行修正避免垃圾记忆污染后续生成。前端的状态管理用了很轻量的方案没有引入 Redux 之类的大框架只用了 Zustand。Zustand 的好处是 API 极简写起来不啰嗦而且对 TypeScript 的支持很好。整个项目的状态其实就三部分文章草稿、生成状态、记忆列表用 Zustand 完全够用。6.2 流式文本的渲染与光标保护前端接流式文本时有一个非常容易踩的坑边生成边渲染会打断用户的输入光标。想象一下这个场景用户正在看模型生成第三章的内容目光盯着屏幕同时他想在生成文本的中间位置插入一段自己的修改。如果流式渲染是一股脑把新文本 append 到内容末尾光标位置会被撑走用户心态直接崩掉。Goink 的解决办法是把生成中的内容和已落盘的正式内容分开维护。生成中的内容暂时作为一个独立的草稿块渲染在编辑器下方用虚线框隔开标明AI 生成中。等生成完成并确定落盘后才把草稿块合并进正式内容区。这样就解决了两个问题一是生成过程中用户的编辑操作不会被打断二是生成结果可以预览确认不满意可以直接丢弃不会污染正文。6.3 记忆面板的过滤与编辑记忆面板在视觉上做得尽可能克制。默认情况下显示全部记忆但提供三个维度的过滤按类别过滤事实/决策/风格/结构、按文章过滤、按关键词搜索。记忆条目的渲染很有讲究每条记忆默认只显示内容的前两行完整内容通过展开按钮查看。这样做是为了避免面板过于冗长因为实际用下来一篇文章可能积累几十条记忆如果全部展开用户根本找不到重点。每条记忆的右上角有一个置信度标识用色块表示高低。用户可以直接点击色块调节置信度系统会即时更新。这个功能表面上看只是个小交互实际上非常有用——当用户发现某条记忆导致模型生成跑偏时直接把这条记忆的置信度调低比删掉它更灵活因为这条记忆将来可能还有用。6.4 离线可用与本地数据优先Goink 的前端做了一个相当实用的设计文章草稿和记忆数据优先保存在本地再与后端同步。具体实现是用 localStorage 做了一级缓存每次编辑都会先写入本地然后防抖 500ms 后同步到后端。这样即使网络断开用户依然可以继续编辑内容等网络恢复后自动同步。这个设计的灵感来自于我用在线文档时的痛苦经历——一旦网络不稳定编辑的内容不知道什么时候就丢了。对于写作这种场景数据的安全感比功能的丰富度更影响实际体验。7. 部署、性能与踩坑实录7.1 生产部署的完整流程Goink 的部署是我特意设计得足够简单的因为它的目标部署者不仅有后端工程师还有完全不懂运维的普通用户。后端编译这一步交叉编译只需要一条命令CGO_ENABLED0 GOOSlinux GOARCHamd64 go build -o goink-server ./cmd/server编译出来的二进制完全静态连接不依赖系统的动态库扔到任何 Linux 机器上都能跑。前端构建npm run build构建产物是一堆静态文件我会让 Go 后端通过embed.FS直接嵌入到二进制里。这样最终交付的时候只有一个文件既是可执行程序又自带前端页面和静态资源。用户只需要下载这一个文件运行它然后浏览器打开地址就能用。反向代理虽然 Go 的net/http可以直接提供服务但生产环境我还是习惯在前面挂一层 Nginx。一是可以统一处理 HTTPS 证书的自动化申请与续期二是可以利用 Nginx 的缓冲来过滤一些攻击流量。Nginx 的关键配置在于SSE 流式请求必须禁用缓冲location /api/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Connection ; proxy_buffering off; proxy_cache off; }重点在proxy_buffering off这一行。如果没有这一行Nginx 会默认缓冲响应导致前端收不到流式数据只能等到整个请求结束才能收到全部文本——这会让流式体验直接退化为转圈等结果这是部署中最容易踩的坑。7.2 内存与性能的实测数据我在一台 1 核 1G 的小服务器上做了压测。测试场景是连续 20 次续写请求每次请求注入约 3000 token 的记忆上下文模型输出平均 500 token。实测结果是指标数值平均单次请求耗时6.8 秒后端进程内存占用峰值78 MB数据库文件大小100 篇文章约 12 MBSSE 推送平均延迟80 ms并发 5 个请求时的 CPU 使用率65%78 MB 的内存占用对 Go 应用来说算是正常偏优的水平。相比之下早期 Python 原型在同样场景下内存占用超过 400 MB差距体现在这里就很直观了。有一点需要注意内存占用和模型的输出长度关系不大主要跟记忆检索时加载的向量数量和文本缓冲区大小有关。如果用户积累了大量记忆内存可能会波动但实测在几千条记忆的规模上没有明显问题。7.3 踩坑实录五个常见问题第一个坑SSE 连接频繁断开。排查后发现是 Nginx 默认的超时设置太短。SSE 连接是长连接如果超过 60 秒没有数据流动Nginx 会主动断开连接。而写作场景中模型 API 的响应时间可能超过 60 秒。解决方法是调大proxy_read_timeout到 300 秒。第二个坑SQLite 的 database is locked 错误。在流式生成过程中我需要频繁追加草稿内容而与此同时前端还会触发保存请求两个写并发一起出现就会冲突。解决方法是引入一个全局的串行写入队列所有写操作按顺序排队执行。实践下来的感受是在 SQLite 上做并发写优化与其用 WAL 模式加锁协调不如从架构上避免并发写。第三个坑向量索引会导致启动变慢。随着记忆量增加每次启动时加载全部向量做索引的时间越来越长。解决方法是把向量索引改成懒加载——只在第一次检测到记忆数量超过阈值时才建立向量索引之后增量更新而不是全量重建。第四个坑模型返回的中文引号被转义。有一次生成的内容里所有的中文引号都变成了\u201c这种转义字符排查后发现是 JSON 序列化处理不当。解决方法是统一封装一个文本解码函数在流式接收时主动处理常见的转义序列。第五个坑长时间运行后内存不降反而缓慢增长。这个问题排查了两天最终定位到是模型 API 的响应体没有及时关闭。每次请求读完响应体之后必须关闭 body否则连接不会被复用而是新建连接长此以往连接资源泄露导致内存不断累积。7.4 这个架构值得改进的方向Goink 目前的架构解决的是单篇文章写作场景下的一致性问题但扩展空间还很大。我最想做的下一步改进是跨文章记忆的共享图谱。目前记忆是和单篇文章绑定的虽然理论上可以跨文章复用但没有形成一个自动的知识网络。如果能把所有文章里的实体拉通构建成一个用户级的知识图谱每次写作时推荐的记忆会更精准甚至能主动提示用户你三月份写过一篇和这个话题相关的文章要不要参考。另一个值得探索的方向是记忆的自我修正机制。目前提炼记忆是即时的、一次性的但用户可能会在之后的写作中推翻之前的决策比如原来我们决定用方案 A但后来发现成本太高改成方案 B 了。这种记忆冲突目前只能靠用户手动修改未来希望能自动识别冲突并询问用户是否更新旧记忆。最后一个实际工作中很实用的改进是导出和导入记忆。如果用户换了新机器或者想把写作项目的记忆迁移到另一个项目里一个标准格式的导出文件会非常管用。我在设计存储层时已经预留了序列化接口实现起来不会太复杂。这个项目做下来我最大的体会是给 AI 加记忆技术难点从来不在存储而在筛选和遗忘。信息存起来很容易难的是知道哪些该被记住、哪些该被淡忘、哪些该在什么时候被想起。Goink 用结构化分类、双通道检索、动态裁剪这套组合拳把记忆从一个玄学概念变成了可度量、可调试、可优化的工程问题。如果你也在做 AI 写作类的工具这个架构可以直接拿来当参考就算不用 Go 和 React核心的设计思路也完全可以平移到你自己的技术栈上。