06. 代码重构实践
为什么需要重构?
在快速实现最小可用闭环(Phase 1 & Phase 2)的过程中,为了尽可能缩短获得反馈的链路,我们不可避免地留下了一些“技术债”与“临时代码”:
- 逻辑重复:
generateId辅助函数在agent.ts、llm.ts、memory/manager.ts中都被重复定义了一次。 - 类型污染与冗余:
MemoryEntry的类型定义同时出现在src/types/index.ts和src/memory/types.ts,导致编译报错或类型推断混乱。 - ESM 路径查找报错:由于使用了原生的 ESM (
NodeNext),我们在进行多包相对路径引入时偶尔遗漏了.js扩展名,导致运行时抛出ERR_MODULE_NOT_FOUND。 - Workspace 依赖协议错误:在
package.json中错误地混用了普通的版本号引用本地包,导致 pnpm 在安装依赖时报ERR_PNPM_FETCH_404错误。 - CLI 对话回归 Bug:在开发斜杠指令拦截时,误将向 Agent 输送普通文本的
agent.run(userInput)链路截断,使得 CLI 只能相应特异指令,失去了正常的聊天功能。
工程开发中,“先跑通,再重构” 是正确的,但重构绝不能无限期延后。因此,本章我们专门停下来进行一次系统性的代码重构。
重构原则
在进行本次重构时,我们严守以下四条工程纪律:
- 不添加新功能:重构的唯一目标是清理结构,绝不在此时夹带私货编写新逻辑。
- 渐进式重构:每次只改动一个模块,改完后立即运行测试脚本
test-phase1.ts与test-phase2.ts,确保没有发生功能倒退。 - 对外接口(API)保持稳定:确保核心暴露的
Agent、MemoryManager外部签名不变,将影响控制在包内部。
主要重构步骤与代码解析
1. 抽取并统一共享工具方法
我们将所有的 generateId 实现删除,统一挪入本地共享包 @hachimi/shared。
在 packages/shared/src/index.ts 中:
export function generateId(prefix = ""): string {
return `${prefix}${crypto.randomUUID()}`;
}在核心包的 packages/core/package.json 中配置引用:
"dependencies": {
"@hachimi/shared": "workspace:*"
}在核心包的所有需要生成 ID 的地方,统一通过别名路径引入,注意必须携带 .js 尾缀:
import { generateId } from "@hachimi/shared"; // 由 tsconfig paths 映射2. 剥离并去重核心类型定义
我们将 MemoryEntry 从局部的 memory/types.ts 中彻底移出,合并至全局类型中。
更新后的 packages/core/src/types/index.ts:
export type MemoryLayer = "working" | "session" | "long_term" | "archival";
export interface MemoryEntry {
id: string;
layer: MemoryLayer;
content: string;
embedding?: number[];
importance: number;
createdAt: number;
lastAccessedAt: number;
metadata?: Record<string, unknown>;
}而在模块私有的 packages/core/src/memory/types.ts 中,只保留检索参数定义,并显式引入全局定义:
import type { MemoryLayer } from "../types/index.js";
export interface MemorySearchOptions {
layers?: MemoryLayer[];
limit?: number;
minImportance?: number;
}3. 规范 pnpm Workspaces 声明
如果只在根目录的 package.json 里写 "workspaces" 属性,pnpm 是不认可的(那是 npm/yarn 的规范)。我们需要在根目录建立 pnpm-workspace.yaml 文件:
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'packages/channels/*'
- 'packages/tools-builtin/*'
- 'packages/storage/*'
- 'apps/*'同时,检查所有本地子包之间的相互引用,版本号必须强制使用 "workspace:*":
"dependencies": {
"@hachimi/core": "workspace:*",
"@hachimi/shared": "workspace:*"
}4. 修复 CLI 的聊天回归缺陷
在 scripts/chat.ts 中,由于逻辑分支编写不当导致正常对话被跳过。我们重构了 while(true) 对话监听体,建立了清晰的分支拦截链:
// scripts/chat.ts 中的主处理逻辑
// 1. 第一层:拦截无意义的空输入
if (!userInput) continue;
// 2. 第二层:拦截并匹配退出指令
if (["/exit", "exit", "quit"].includes(command)) {
sessions.save();
console.log("再见!");
break;
}
// 3. 第三层:拦截记忆相关的管理命令 (/memories, /remember)
if (command === "/memories") {
// ... 显示记忆
continue;
}
if (command.startsWith("/remember ")) {
// ... 快捷记忆
continue;
}
// 4. 第四层:兜底的正常对话链路
const history = sessions.getHistory();
const reply = await agent.run(userInput, history);
console.log("hachimi:", reply);重构后的收益
- 代码量减少:消除了三个文件里重复的 ID 生成和随机数生成器。
- 模块拓扑关系清晰:
shared位于底层,core依赖shared,apps(CLI)位于顶层组装,无循环依赖。 - 消除编译警告:修复了所有的原生 ESM 相对路径解析,在执行
pnpm typecheck时顺利通过,零 Warning。
本章总结
通过本章的主动“代码还债”:
- 我们规范了 Monorepo 的包定义和依赖描述。
- 提取了核心类型,解决了类型定义重复导致的工程报错。
- 修复了命令行终端交互的逻辑缺陷,并跑通了完整的回归测试。
重构完成,我们的 Harness 结构变得极为健康。接下来,我们将迈出极为关键的一步——接入真实 LLM。