ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用Rust构建高性能AI助手核心:架构、异步与工具调用实战

用Rust构建高性能AI助手核心:架构、异步与工具调用实战

1. 项目到底在做什么:先讲清楚AI助手的核心定位

1.1 用Rust做AI助手核心,到底图什么

先聊个很多读者问过的问题:市面上做AI助手、AI Agent的,十个里有九个用Python,凭什么我们用Rust从0到1写一个核心?

我的答案很直接:我要的是一个能拿出去分发、低资源占用、不容易崩的本地助手程序,而不是一个依赖一堆Python包、启动慢吞吞的解释器脚本。

Python写AI应用确实快,但换来的代价是大。你打包一个PyInstaller出来的东西,体积动辄两三百兆;跑起来之后,模型还没调用,光解释器和依赖库就已经吃掉了接近200MB内存;更别提在Windows上打包时各种动态库缺失的玄学问题。Rust这边,编译出来就是一个静态链接的二进制文件,Windows下release版本大概5到12MB,Linux下更小,加上tokio异步运行时,整体内存占用常常能压在60-80MB以内。对于“本地AI助手”这种定位,这个差距是决定性的。

还有一个被很多人低估的点:静态分发带来的部署简洁性。你不需要让用户先装Python 3.10以上,不需要处理virtualenv,不需要pip install那些有可能编译失败的包。把二进制丢给用户,双击就能跑,这才是“助手工具”该有的形态。这也是为什么现在tauri + Rust做桌面应用这么火,底层逻辑是一样的。

当然,选择Rust也意味着开发速度会比Python慢。这个我在前16课里已经反复强调过——我们的策略是“先用Python做原型验证,再用Rust实现正式版”,把试错成本留在Python侧。从第17课开始,我们就正式把重心全部移到Rust实现上。

1.2 技术边界:做连接、调度、编排,而不是做模型

项目名叫“AI助手Rust核心”,很多新手拿到名字就开始迷茫:我是不是要用Rust写一个Transformer?要训练模型?要搞推理引擎?

完全不需要。

我们要做的是一个中间层:上面接大模型(包括本地通过Ollama跑的模型,或者远程OpenAI兼容接口),下面接用户的需求,中间负责上下文管理、工具调用、记忆存取、结果整理。模型的推理能力是别人提供的,我们负责的是“怎么让模型用得顺手、可扩展、可控”。

打个比方:模型像是厨师,AI助手核心像是餐厅的前厅系统。你不需要自己会炒菜,但你要知道菜单上有哪些菜、客人点了单怎么传给后厨、后厨出菜之后怎么端到客人桌上、客人吃完了怎么结账。前厅系统做得好不好,直接决定餐厅能不能规模化运作。

这个边界一划清楚,整个项目的工作量就明朗了:

  • 模型网关:负责与不同来源的模型通信,统一输入输出格式
  • 会话管理:维护多轮对话的上下文,控制窗口长度
  • 工具系统:让模型能够调用外部能力,比如查天气、算数学、读文件
  • 记忆存储:跨会话保留有用的信息
  • CLI交互层:接收用户输入,展示结果

每一块都不需要研究AI算法本身,但每一块都需要工程上的扎实实现。这正是Rust擅长的地方。

1.3 整体模块划分与数据流向预览

这个项目不是一大堆文件堆在一起,而是按职责分成几个相互独立的模块。我在设计的时候直接采用了Cargo workspace结构,每个核心模块是一个独立的crate,这样好处很多:编译单元清晰、依赖关系明确、方便单独测试。

整个核心可以概括为五个主要模块加一个入口:

  • assistant-cli:命令行入口和交互循环(REPL)
  • assistant-core:核心逻辑,包括会话管理、上下文组装、API调用编排
  • assistant-tools:工具注册与函数调用执行器
  • assistant-memory:持久化记忆与历史会话存储
  • assistant-gateway:不同模型提供方的统一接入层

用户的一条消息从输入到返回,数据流是这样的:用户在终端敲下一句话,CLI层把它交给core层;core带着当前会话上下文,向gateway发起模型请求;模型可能返回普通文本,也可能返回一个工具调用请求;如果是工具调用,core把请求分发给tools层,拿到工具执行结果,再把结果作为新的上下文回合继续请求模型;最终模型给出答案,core把完整会话写入memory,CLI层把答案展示给用户。

这个流程里最核心的设计决策是:**模型调用和工具调用是一个循环,直到模型不再请求调用工具为止。**这个循环边界控制、超时设置、次数上限,是我们在后面课程里要重点实现的细节。

2. 核心架构逐层拆解:每一层都在解决什么问题

2.1 CLI入口层:命令解析与交互循环

先看用户最先接触到的这一层。CLI层看起来简单,但我在设计时用了不少心思。

交互模式采用类REPL的设计,就是像Python REPL那样,用户输入一行,程序处理一行。但AI助手的交互比普通REPL复杂,因为一次“助手回复”可能触发多次内部模型调用(多轮工具循环),用户等待时间会比较长。所以CLI层做了一个状态提示机制:在模型思考期间,利用indicatif库显示动态进度条,并且把当前阶段(正在请求模型、正在执行工具)显示出来,避免用户以为程序卡死了。

命令解析这一块,我用的是clap这个库。它会自动生成命令行的--help、参数校验、子命令分发,比自己手动解析省事太多。但CLI层不只是技术入口,它还承担了人机交互的体验设计:

ai "帮我写一个Python快速排序" ai --model qwen2.5:7b "解释一下这段代码" ai --tool on "算一下 12345 * 6789" ai --list-sessions ai --resume 20250201-ai-helper

每个参数都对应一个核心能力:--model切换模型,--tool开启工具调用,--list-sessions查看历史会话。交互模式则直接输入指令进入REPL循环。另外我还实现了--output json模式,方便脚本调用这个助手做自动化任务——这个设计是吸取了很多AI工具的经验,终端工具的数据要结构化,才能被别人编排。

2.2 配置与密钥管理:哪些能入库,哪些绝不能入库

每个AI项目都会面临一个问题:模型API的地址、密钥、默认参数该放哪里?这个模块我踩过坑,值得专门讲。

配置采用分层策略:**默认值编译进程序,用户配置覆盖默认值,环境变量覆盖用户配置,命令行参数优先级最高。**比如默认的模型地址是127.0.0.1:11434(Ollama默认端口),如果你自己搭了服务,可以改配置文件;如果临时想用某个特殊地址,可以在命令行直接指定。

配置文件用TOML格式,放在系统标准配置目录。Rust里可以用dirscrate来定位这个目录,跨平台表现很稳:

[model] provider = "ollama" model_name = "qwen2.5:7b" temperature = 0.7 max_tokens = 4096 timeout_secs = 120 [gateway] remote_api_base = "" api_key = "" [memory] enabled = true max_history_sessions = 50

这里有一个安全铁律:**api_key不能写在代码里,不能提交到git仓库,配置模板里只放空字符串占位。**我在项目的.gitignore里强制忽略了config.toml和.env文件。API密钥的读取顺序是:环境变量优先,其次才读配置文件。这样即使配置文件不小心泄露了,只要密钥放在环境变量里,风险就小很多。

2.3 会话管理与上下文窗口控制

这是整个项目中“看起来简单,做好很难”的部分。模型无法记住所有历史对话,它只有有限的上下文窗口。Qwen这类模型可能是8K或32K token,Claude这些更长的也就是200K token。我们不能无脑把所有历史都丢进去。

我设计了一个Session结构体,核心字段是:会话id、系统提示词、消息历史(按角色分组)、创建时间、最后活动时间、token估算总量。在每轮对话组装请求前,会调用一个estimate_tokens函数估算当前历史占用,如果超过阈值(比如窗口的70%),就启动裁剪策略。

裁剪策略我实现了两档:粗裁剪和摘要压缩。粗裁剪是直接从最早的非系统消息开始丢弃,以保证不爆窗口;摘要压缩是调用模型本身,对较旧的多轮对话生成一段摘要,替换压缩后的原始消息。摘要压缩效果好,但会额外消耗模型调用次数,所以我把这个策略放在粗裁剪之后仍超限的情况下才启用。

这里面的关键技巧是:系统提示词永远不参与裁剪。系统提示词定义了助手的角色、风格、可用工具列表,这是每轮请求都必须完整保留的。很多新手在实现上下文管理时,会把系统提示词当作普通消息一起裁掉,导致模型行为漂移。

2.4 模型网关层:本地Ollama和远程OpenAI兼容接口

模型网关层是整个系统的“适配器”,它的存在价值就是屏蔽不同模型提供方的差异。我们的核心策略是:对外统一,对内适配。

先说统一。我给gateway层定义了一个内部的数据模型,字段大概是:role(system/user/assistant/tool)、content(可空)、tool_calls(可空)、name(工具名)。这样不管底层调的是Ollama、OpenAI、还是别的兼容接口,在core层看来都是同一批结构体。

再说适配。Ollama暴露的是/api/chat接口,请求体结构跟OpenAI不完全一样,返回的流式格式也不同。OpenAI兼容接口则是标准的/v1/chat/completions。我们在gateway里维护一个Provider枚举——Ollama和OpenAICompatible——每个变体实现各自的请求构造、响应解析、错误映射逻辑。当用户新增一个模型提供方时,只需要加一个Provider变体,实现对应的方法即可,不需要碰core和CLI。

实际实现中有一个特别值得分享的点:**不要把所有响应字段都声明为强制字段。**不同模型、不同版本,返回的JSON字段经常会有细微差异,比如有的返回delta.content,有的返回message.content,还有的多一个reasoning_content。如果字段都用serde直接反序列化,一个字段对不上就整个解析失败。建议把关键字段定义为Option,次要字段直接忽略。宁可少拿一个候选信息,也不能让整个请求因为格式抖动而崩掉。

2.5 工具注册与Function Calling调度器

工具调用是让这个AI助手从“聊天机器人”升级为“能干活的助手”的关键能力。模型本身不会直接执行外部操作,它只是在回答中输出一个结构化请求,比如{"name": "calculate", "arguments": {"expression": "12345*6789"}},然后由我们的程序解析并执行。

要支持动态、可扩展的工具列表,我用了宏加trait组合的方式。定义一个Tooltrait,核心方法是name()、description()、execute(args)。然后通过一个ToolRegistry结构体来管理所有注册的工具:

#[async_trait] pub trait Tool: Send + Sync { fn name(&self) -> &'static str; fn description(&self) -> &'static str; fn parameters_schema(&self) -> serde_json::Value; async fn execute(&self, args: &serde_json::Value) -> Result<serde_json::Value, ToolError>; }

注册中心维护一个BTreeMap<String, Box<dyn Tool>>,每次向模型发送请求时,把注册表里所有工具的name、description、parameters_schema拼成一个工具列表塞进请求。模型看到这个列表,就知道“现在有这些能力可用”。

执行调度器那边,我设定了一个循环上限,默认最多执行8轮工具调用。为什么?因为如果模型陷入“调用工具-得到结果-再调用工具”的死循环,程序永远不会结束。设置上限后,超过次数就强制返回当前结果,宁可结果不完美,也不能让用户等一晚上。

2.6 记忆与存储层:跨会话的持久化

没有记忆的AI助手就像每次都要重新认识你的人。这个模块负责把每次会话做完结性存储,并且支持下次启动时快速恢复。

存储方案我选择了SQLite,理由有三个:一是单文件、零配置,适合本地工具;二是Rust生态有rusqlite,同步API稳定好用;三是数据量本身不大,不需要上PostgreSQL这种重型方案。

表结构设计了三张表,分别是sessions(会话主记录)、messages(每轮消息内容)、key_values(键值对存储,用来放全局偏好、用户自定义系统提示词)。所有写操作都在单独的线程池里执行——是的,我们用了tokio::task::spawn_blocking包了一层,避免SQLite的同步操作阻塞异步核心。

触发的时机也讲究:不是每次用户发消息就立刻写库,那样IO太重。我的做法是:每轮对话结束后,由core层发一个“会话更新事件”,memory模块收到事件后在后台异步落盘,用户完全感知不到存储过程。如果在一次会话过程中程序崩溃了,最多丢失当前这轮的对话,已完成的回合不会丢。

3. 关键技术决策与取舍:为什么这么选而不那么选

3.1 异步运行时:tokio几乎是标准答案

异步运行时我选了tokio,这个基本没什么悬念。当前Rust异步生态里,tokio的社区规模最大、文档最全、周边库适配最广,几乎没有哪个做网络服务的Rust项目不用它。async-std和smol有它们更轻量的优势,但在这个项目里,选择tokio意味着reqwest、rusqlite这类库的异步对接更顺畅,遇到问题时搜到的解决方案也最多。

但用tokio不意味着可以随便写。一个非常容易犯的错误是:**把CPU密集型的同步代码直接丢在async函数里执行。**比如解析超大的JSON、正则匹配长文本,这些操作如果直接写在异步函数中,会阻塞tokio的工作线程。tokio默认工作线程数是CPU核心数,你阻塞一个,就减少一个可用线程;如果所有工作线程都阻塞了,整个异步任务就停摆了。

解决方案也简单,两招:一是用spawn_blocking把阻塞操作送到专门的阻塞线程池,结果通过await取回来;二是为模型请求设置超时。我们统一用tokio::time::timeout包装所有HTTP请求,超时时间在配置里给默认值120秒,防止出现“服务端黑洞导致程序永久挂起”的极端情况。

3.2 HTTP客户端与JSON处理:reqwest加serde是黄金搭档

HTTP客户端我用reqwest,JSON处理用serde加serde_json。这两个可以说都是Rust生态里的“事实标准”,组合起来非常省心。

reqwest有几个在这个项目里特别有用的特性:支持连接池复用(同一个Host的多个请求复用TCP连接,减少握手开销)、支持超时控制、支持流式响应读取(对大模型输出的增量渲染至关重要)。我把流式响应的处理放在gateway层:从网络收到一个chunk,就解析一次增量JSON,追加到缓冲区,对外通过tokio::sync::mpsc通道发给core层。这样用户体验是打字机式的逐字输出,而不是等全部生成完才看到一坨文本。

serde这边有一个细节经验:**写反序列化结构体时,多花十分钟把#[serde(default)]和#[serde(rename_all = "camelCase")]这些属性标好。**很多模型API返回的JSON字段命名是camelCase,而Rust惯例是snake_case,没有rename属性就得手写一堆别名逻辑,容易出错。标注属性后,一行代码都不用写就自动映射了。

3.3 错误处理:双轨制是Rust项目的成熟做法

在Rust项目里错误的处理方式,往往是判断一个团队是否成熟的试金石。我采用的双轨制是:业务逻辑层的错误用thiserror手写错误类型,顶层函数用anyhow::Result做汇总集成。

具体来说,gateway层定义了自己的错误类型,比如GatewayError::NetworkTimeout、GatewayError::Unauthorized、GatewayError::MalformedResponse,每个错误都能明确告诉上层“发生了什么、能不能重试”。工具执行层也有ToolError,区分参数缺失、执行失败、超时等场景。

到了CLI层,这些精确错误并不需要展示给终端用户,只是unwrap的话又会丢信息,所以用anyhow在顶层统一收集。anyhow::Context这个trait非常实用,它允许你在错误链上附加上下文提示。比如“读取配置文件失败”这种外层信息,能让用户快速定位问题,而不用去翻内部错误码。

3.4 一个容易翻车的核心难点:非流式与流式结果的结构化解析

对话模型输出里除了普通文本,还可能包含“工具调用请求”,这块解析是整个项目里最容易翻车的地方之一。

原因在于:同样是Qwen,非流式模式下返回的JSON里工具调用字段是完整的tool_calls数组;切换到流式模式后,同一个工具调用被拆成了多个chunk,可能第一个chunk只出现了name的一半,第二个chunk才拼完arguments的JSON字符串。如果按普通文本的拼接方式处理,工具调用的JSON很可能是断裂的。

我的解法是:在流式模式下,先不直接解析JSON,而是把所有delta片段按顺序拼接存储;只有当流结束时,才对完整的tool_calls字段做一次标准化解析。同时,对解析出来的arguments字符串做一个额外的校验——用serde_json::from_str尝试解析,失败则标记为“工具参数格式错误”,并把这个错误信息回传给模型,让模型自己修正参数后重试。这个“错误回传模型”的设计,是Function Calling稳定性的关键,很多实战项目都在这上面踩过坑。

3.5 依赖清单与Cargo workspace组织方式

最后把开箱的依赖清单列一下,方便想直接上手复现的读者:

类别选择说明
异步运行时tokio带full features
HTTP客户端reqwest开启json、stream特性
JSON处理serde、serde_json必配derive
错误处理thiserror、anyhow双轨制
CLI解析clapderive模式
交互提示indicatif进度条与状态提示
配置解析toml、dirs用户配置读取
数据库rusqliteSQLite、bundled特性
日志tracing、tracing-subscriber结构化日志

workspace组织采用根目录的Cargo.toml声明成员,各子crate独立管理依赖。这个结构的核心收益是编译速度:改动一个子crate时,不需要重建其他crate;每次保存代码后增量编译的时间能控制在几秒内,对开发体验提升非常大。

4. 从0到1的路线图:前16课做了什么,第17课之后走向何方

4.1 阶段一:Rust语言地基(对应第1到5课)

这个阶段没有碰任何AI内容,纯粹是把Rust语言本身打牢。第1课从环境安装说起:包括rustup工具链安装、cargo命令、Windows下还需要装Visual Studio Build Tools来提供链接器。这个环节有不少人卡住,因为安装Build Tools是典型的“看着莫名其妙但缺了没法用”的步骤。

第2到4课讲所有权与借用、生命周期、模式匹配、trait。这些是Rust区别于其他语言的灵魂。我个人经验是:**不要试图一次全懂,先写起来,写错编译器骂你,骂着骂着就理解了。**第3课我布置了一个“实现一个简易记账本”的练习,就是为了让所有人脱离语法书、真正动起来。

第5课是模块系统和错误处理。项目从这一课开始就从“一个个demo”转向“能组织的工程”。我要求所有学员从第5课开始,每个练习都用workspace结构组织代码,这个习惯到后面第17课这个正式项目里直接受益。

4.2 阶段二:异步与工具链(对应第6到10课)

第6课正式进入async/await。先是讲Future的概念,再是tokio的runtime模型。我特意用一个“并发下载多个网页,取最快返回结果”的例子来解释join!和select!的区别,这个例子直观且贴近需求。

第7课是JSON与数据序列化。serde从derive到自定义反序列化,再到处理那些“脏数据”。这里就开始为后面的模型API对接做准备了。

第8课讲CLI开发,引入了clap和indicatif。第9课做SQLite集成,用rusqlite实现增删改查,把数据持久化彻底搞清楚。第10课做集成——把前面的CLI、异步、数据库串起来,写了一个简易的“命令行Todo+日记”工具。

这套路线的关键用心在于:**每一步都能产出独立的、有成就感的作品,而不是永远都在学语法。**异步和数据库的配合在第9课就练过,所以到第17课的memory模块基本是熟悉路径,开发速度明显快。

4.3 阶段三:AI能力接入(对应第11到16课)

从第11课开始碰真正的大模型编程。第11课先在本地部署Ollama,拉取模型,用curl手工调一次API,理解请求与响应结构。这个过程让很多人第一次意识到“大模型不过是一个HTTP服务”。

第12课用reqwest在Rust里完成同样的调用,同时引入流式响应处理。第13课把“多轮对话的历史消息管理”做了出来——这就是上下文管理的雏形。第14课开始做工具调用(function calling),用加法计算器和当前时间获取两个简单工具做试验,让模型学会输出工具请求、让程序学会解析执行。

第15课把工具系统重构成了可以动态注册的结构,并加入工具参数schema的自动生成。第16课做了错误处理和日志体系。到第16课结束,一个“能跑、能吃住简单工具调用、有基础日志”的雏形已经成型了。第17课这篇总览,就是把前面零散的模块拼装成清晰的架构认知,为后面新增复杂功能做框架准备。

4.4 第17课之后的扩展规划

第17课本身不写新功能,它的产出是一份架构文档加一个“重构版”的骨架仓库。之后的内容大概分成四块:

第一块是工具生态扩展,实现文件读写工具、HTTP请求工具、数据库查询工具,把手头的助手从“会聊天”变成“会干活的终端代理人”。

第二块是更聪明的上下文压缩,把跨会话的记忆摘要做扎实,让助手真正做到“上次聊的东西这次还接得上”。

第三块是多种模型切换和自动路由,比如按任务难度自动选用本地小模型和远程大模型,控制成本和速度的平衡。

第四块是打包与分发,用GitHub Actions做跨平台release构建,输出Windows、Linux、macOS三个平台的可执行文件,做一个真正能发给别人的产品。

5. 常见问题与踩坑实录

5.1 异步里跑SQLite,程序“卡死”了

刚开始实现memory模块时,我直接在async函数里调用rusqlite的同步方法,结果单测跑得好好的,一接入真实对话,并发两轮就卡顿明显,甚至出现整个进程假死。原因前面已经提过:同步操作阻塞了tokio的工作线程。

解决方式是把所有数据库操作包进spawn_blocking。还有一个容易被忽略的点:**rusqlite的Connection不能被多个线程同时使用。**如果多个任务往同一个连接里写数据,会直接panic。我在实践中的方案是给每个会话维护一个独立的连接,或者用Arc<Mutex<Connection>>做全局串行化,具体取决于并发度。本地单用户场景下,Mutex就够了,性能损失可以忽略。

5.2 模型返回的工具调用参数是坏的JSON

这个问题我在第14课第一次整合工具调用时遇到过。流式输出回合结束后,我尝试serde_json::from_str解析arguments,结果一直报错。仔细排查后发现,有些小模型输出的JSON是“残缺的变体”,比如字符串没闭合、多了尾随逗号、混入了markdown代码块标记。

这个问题的最终解法分两层:先把可能包裹的markdown代码块剥离掉,再做一次宽松解析;实在解析失败,就把错误信息拼接成一条“工具参数解析失败”的提示,回传给模型让它修正后重试。我后来测试下来,大多数重试都能得到可用的参数。从没想过“让模型自己纠错”这个操作居然是最实用的容错方案。

5.3 Windows下控制台中文乱码

在Windows上运行这个助手时,直接打印中文会出现乱码,这是Rust项目跨平台的经典坑。原因在于Windows控制台默认代码页是GBK,而Rust字符串是UTF-8。

最简单粗暴的解决方式是:在CLI初始化时调用一次windowsAPI手动设置控制台输出代码页为UTF-8,或者直接告诉用户在终端里执行chcp 65001。更规范的做法是使用crossterm这类终端库,接管了编码问题。我的实现是在CLI启动后先做一个平台判断,Windows下调用SetConsoleOutputCP(65001),实测可以彻底解决乱码。

5.4 配置文件里的密钥被提交到git

这是我最尴尬的一次事故。早期做CI测试时,顺手把包含占位密钥的config样例提交了,本身只是占位符问题不大,但养成的习惯很危险。后面加真实API key时,我的.gitignore已经配置完善了,但对config.toml这种带密钥的文件必须保持零容忍。

推荐的做法是:仓库里只保留config.example.toml,里面全部是占位符,config.toml直接在.gitignore里忽略。CI发布时,用echo加环境变量动态生成配置文件。这个经验虽然简单,但能避免很多后续麻烦。

5.5 日志淹没在调试输出里

测试models调用时,tracing的输出铺满整个终端,有用的信息全被淹没了。解决方式是采用“分层日志”:调试时把RUST_LOG=debug开启细粒度日志;正常运行时默认info级别,只输出关键节点——启动配置、模型选择、每轮请求耗时、工具调用结果。调试特定模块时再加RUST_LOG=assistant_core=trace这种模块级过滤,效果比全量debug好太多了。

6. 关于这门课、这个项目,最后分享一点我做事的体会

做到第17课,有个特别深的感触:用Rust写AI助手,难点不在于“AI”而在于“工程”。模型API大家都差不多,真正的区分度在上下文管理得够不够精细、工具系统扩展起来够不够顺手、程序在各种网络异常和格式异常下能不能稳定运行。Rust逼着你把这些边界情况都处理干净,编译器甚至会在你写之前就逼你想清楚某些类型逻辑,这对做AI应用开发来说是很好的约束。

如果你正打算跟着这个系列从0开始,我给三条实实在在的建议:

第一,跟着路线图走,前16课每一课的小项目都别跳。每一课的小项目都在为后面的正式项目铺路,跳过链路上任何一环,到第17课都会补课补到怀疑人生。

第二,本地模型和远程API两条路都跑一遍。本地模型(Ollama)能让你随时调试、不花钱、断网也能跑;远程大模型则让你体验到更高质量的答卷和更复杂的格式输出。两边都摸一遍,对“模型提供商”这个角色的理解才算完整。

第三,第三点也是我自己的习惯:每完成一个模块,就尽量把关键决策记在项目根目录的docs/decisions.md里,哪怕就两三句话。比如今天你决定轨迹用SQLite而不是文件存储,明天你可能就忘了当初为什么这么选。这个文件在项目后续扩展时,价值高到没法估量。

这个核心从0到1做到这里,地基已经稳了。接下来的路,是把工具做多、把记忆做深、把体验做顺。还是那句老话:先跑起来,再跑好。

返回列表