一个用 Rust 写的「每日一题」命令行工具:每天出一道针对你薄弱点的面试题 → 编辑器作答 → AI 评分反馈 → 后台分析薄弱点 → 明天的题自动偏向薄弱点。

一、要解决的问题
面试准备最大的痛点是「不知道练什么」:
- 刷题不针对:通用题库按知识点随机出,反复练自己已经会的
- 练完没反馈:写完答案没人告诉你答得怎么样、差在哪
- 薄弱点靠感觉:全凭印象判断自己哪里弱,没有数据支撑
- 刷题要不显眼:在职准备跳槽的人,浏览器里开着刷题页面,一眼就暴露意图;终端窗口和日常敲代码毫无差别,工作间隙也能自然练习
daily-q 面向后端/数据方向的程序员,把「出题 → 作答 → 评判 → 复盘」闭环进一个命令行工具,核心循环:
每天出 1 道题 → 作答 → AI 评分反馈 → 后台分析薄弱点 → 下次出题自动偏向薄弱点
典型场景:在职后端程序员想跳槽,午休或工作间隙在终端敲下 dq 就能完成一整套练习------出题、作答、评分都在一个黑框里,看起来和日常写代码没有任何区别,不必背着同事偷偷打开浏览器刷题。
实际运行效果(隔离环境 + mock LLM,可复现):
bash
$ dq # 今日无题,LLM 按画像+薄弱点动态出题
今日题目 TCP 三次握手 medium
请描述TCP三次握手的过程
$ dq answer # 编辑器作答,AI 评判
请在编辑器中作答,保存并退出 (难度: medium)
得分 75
反馈 整体正确,细节不足
参考答案 SYN -> SYN-ACK -> ACK
[summary] 完成 --- 1 个薄弱项已更新
答完题 dq stats 能直接看到掌握度数据,谁强谁弱一目了然:
bash
$ dq stats
掌握度统计
[~] TCP 1/2 ( 50%) [一般]
[+] 三次握手 1/1 (100%) [掌握]
最新总结
薄弱知识点 TCP
建议
- 多练习传输层

二、解决方案:项目与技术架构
定位 :单文件 CLI,零运维,数据全部在本地 ~/.daily-q/。技术栈 Rust + SQLite + LLM (OpenAI 兼容 /v1/chat/completions 协议,可接国内任意兼容服务商)。
数据流
用户执行 dq
│
▼
quiz::get_or_create_question()
├─ 今天已有题? ──YES──► 直接返回
▼ NO
llm::generate_question() ◄── 薄弱知识点、规则、画像
▼
db.insert_question() ──► SQLite
▼
显示题目 ◄── 用户执行 dq answer
▼
编辑器作答 → llm::judge_answer() → 评分+反馈+标签
▼
db.insert_answer() ──► 更新 topic_mastery
▼
summary::spawn_background_summary()(后台线程)
▼
llm::summarize_history() → 薄弱点/关联/建议 ──► summary_cache
模块职责
| 模块 | 职责 | 行数 |
|---|---|---|
| main.rs | CLI 入口,clap 命令路由 | ~419 |
| config.rs | 配置读写 + 7 个服务商预设 | ~251 |
| db.rs | SQLite 5 张表增删改查 | ~385 |
| llm.rs | LLM 调用(出题/评判/总结)+ JSON 提取 | ~264 |
| quiz.rs | 出题/评判/统计业务逻辑 | ~247 |
| summary.rs | 后台线程触发 LLM 总结 | ~49 |
| profile.rs | 交互式画像引导 | ~139 |
| models.rs | 数据结构定义 | ~109 |
| lang.rs | 中文文案(texts! 宏) | ~187 |
| display.rs | 终端颜色输出 | ~38 |
四个关键设计取舍
1. 选 CLI 而不是网页 / GUI
刷题工具的主流形态是网页端或 App,daily-q 刻意反着来:终端即界面,dq 一个命令走完出题、作答、评分、复盘全流程,启动快、可进 shell 历史、可脚本化。最重要的是不显眼------终端窗口和日常开发毫无差别,在职准备面试时不引人注意,这是 CLI 形态对一个「在职跳槽」用户最实在的价值。
- 得到:启动快、可脚本化、界面隐蔽、数据全本地、零运维
- 放弃:可视化图表、鼠标点选。交互短板用
prompt_select()交互式问答(画像、服务商配置)弥补
2. 无本地题库,全由 LLM 动态出题
放弃维护题库,出题完全交给 LLM:把「画像 + 薄弱知识点 + 自定义规则 + 难度」拼进 prompt,每次生成一道针对性的题。
- 得到:个性化、零题库维护成本
- 放弃:题库的稳定性与可离线(依赖 LLM 可用性,可接受的代价)
3. 后台线程做 LLM 总结,但必须 join
答完题后让 LLM 分析全部历史是异步任务(几百毫秒到十几秒)。用 std::thread::spawn + 独立 tokio current_thread runtime 不阻塞终端;但 CLI 进程在 main 返回后立即退出,detach 线程会被杀死导致总结丢失,所以答题流程末尾必须 join() 等落库。
- 得到:作答反馈即时返回,总结不丢
- 放弃:完全异步的「零等待」(进程生命周期决定必须同步收尾)
4. 配置「一键 + 测试通过才保存」
配置三要素(api_key / base_url / model)缺一不可还容易填错。dq config ai 一键完成:选服务商 → 输 Key → 自动带出 base_url 与默认模型 → 调 LLM 连通性测试 → 通过才保存(配错不落盘,马上重来)。内置 7 家 OpenAI 兼容服务商:
bash
$ dq config base-url list
常用 API 服务商
1. deepseek https://api.deepseek.com/v1(默认模型:deepseek-v4-flash)
2. siliconflow https://api.siliconflow.cn/v1(默认模型:Qwen/Qwen2.5-72B-Instruct)
3. openai https://api.openai.com/v1(默认模型:gpt-4o-mini)
4. moonshot https://api.moonshot.cn/v1(默认模型:moonshot-v1-8k)
5. zhipu https://open.bigmodel.cn/api/paas/v4(默认模型:glm-4-flash)
6. qwen https://dashscope.aliyuncs.com/compatible-mode/v1(默认模型:qwen-turbo)
7. ollama http://localhost:11434/v1(默认模型:qwen2.5)
存储与 LLM 交互
存储 :SQLite 单文件 5 张表,数据在 ~/.daily-q/:
| 表 | 作用 |
|---|---|
| questions | 每日一题(date 唯一约束,一天一条) |
| answers | 作答记录 + score + topic_tags(JSON 字符串) |
| topic_mastery | 知识点掌握度(total / correct / last_practiced) |
| summary_cache | 最新学习总结(薄弱点 / 关联 / 建议) |
| rules | 用户自定义出题规则 |
LLM 交互统一为 JSON (llm.rs 的 chat_json<T>,直接反序列化为结构体):
json
// 出题
{"topic":"TCP 三次握手","difficulty":"medium","question":"...","reference_answer":"..."}
// 评判
{"score":75,"feedback":"整体正确,细节不足","topic_tags":["TCP","三次握手"]}
// 总结
{"weak_topics":["TCP"],"related_weaknesses":[...],"suggestions":["多练习传输层"]}

三、期间面临的挑战
1. 后台总结线程被进程退出杀死
- 背景:
std::thread::spawn启动总结线程后 main 返回 - 怎么错的:进程退出直接杀掉 detach 线程,
summary_cache永远写不进去;本地快速跑好像没问题,真实使用总是丢总结 - 结论:CLI 进程退出会杀死所有后台线程 。
spawn_background_summary()返回 JoinHandle,答题流程末尾join()等落库
2. mock 掩盖的真实 bug:LLM 不按格式返回
- 背景:本地用 mock server(固定返回 JSON)测试 38 条全绿
- 怎么错的:换真实 LLM 后,三个 system prompt 没写「只返回 JSON」约束,LLM 自由发挥返回了题目数组 / 夹杂文本,反序列化直接失败
- 结论:mock 的固定返回是测试盲区。测试全绿 ≠ 上线没问题,必须在真实环境跑一遍端到端;同时把 JSON schema 写进 system prompt 显式约束
3. LLM 输出天生不靠谱,JSON 要逐级兜底
- 背景:LLM 可能把 JSON 包在 ```json 代码块里、前后带废话、括号嵌套
- 怎么错的:直接
from_str解析经常失败 - 结论:写多级兜底的
extract_json------先找 ```json 代码块 → 找普通代码块 → 从第一个{/[按括号匹配截取 → 原样 trim;max_tokens提到 4096,避免长输出截断把 JSON 切碎
4. 配置步骤太多,用户根本走不完
- 背景:早期配置拆三步手动填 base_url,还容易填错(多打
/v1后缀、记错服务商地址) - 怎么错的:从安装到能用要对着文档抄 URL,摩擦巨大
- 结论:
dq config ai一键配置 + 内置服务商预设 + 测试通过才保存,配错不落盘、马上重来
5. 本地零警告,CI 却红
- 背景:本地 Rust 1.88 下
clippy -D warnings零警告 - 怎么错的:GitHub stable 是 1.97,新版 clippy 新增
collapsible-if/useless-borrows-in-formatting两个 lint,CI 直接 5 处报错 - 结论:本地工具链旧不代表 CI 过。CI 用
stable时把「版本漂移」当常态,失败后按 lint 建议升级代码即可
四、经验收获
- 每个技术决策都写下「放弃了什么」:决策记录里每条都列了备选方案与否决理由,日后不反复纠结,也方便别人 review。
- mock 全绿只是起点,真实环境端到端必跑:mock 的确定性会掩盖 LLM 的不确定性,固定响应是测试的死角。
- CLI 的进程生命周期是隐性约束:后台线程、异步任务在进程退出时会失效,设计时就要想好「进程没了怎么办」。
- 对 LLM 输出做最坏假设:显式 JSON schema + 多级解析兜底 + 足够大的 max_tokens,缺一不可。
- 工具要「从安装到第一次用」零摩擦:一键配置、默认值、测试通过才保存,决定一个工具能不能被真的用起来。
- 把发布交给流水线 :打
v*标签 → macOS x64/arm、Windows 32/64 四平台制品 + checksums 自动构建上传,发版成本趋近于零。
五、代码与安装
仓库:https://github.com/Angryshark128/daily-q (MIT)
依赖:Rust ≥ 1.88
bash
# 方式一:GitHub Releases 下载对应平台压缩包(macOS x64/arm、Windows 32/64)
# https://github.com/Angryshark128/daily-q/releases
# 方式二:源码编译
git clone https://github.com/Angryshark128/daily-q && cd daily-q
cargo build --release
# 产物:target/release/dq(建议加入 PATH)
# 使用
dq config ai # 一键配置 AI(选服务商 → 输 Key → 测试通过后保存)
dq profile setup # 设置面试画像
dq # 出今天的题
dq answer # 作答 + AI 评分
dq stats # 掌握度与学习建议
当前状态:v0.1.0 已发布,出题 / 评判 / 后台总结已通过真实 LLM(DeepSeek)端到端验证;发布流水线覆盖 macOS x64/arm 与 Windows 32/64 四平台。后续方向是完善掌握度算法与更多自定义规则。