Skip to content

06. 代码重构实践

为什么需要重构?

在快速实现最小可用闭环(Phase 1 & Phase 2)的过程中,为了尽可能缩短获得反馈的链路,我们不可避免地留下了一些“技术债”与“临时代码”:

  1. 逻辑重复generateId 辅助函数在 agent.tsllm.tsmemory/manager.ts 中都被重复定义了一次。
  2. 类型污染与冗余MemoryEntry 的类型定义同时出现在 src/types/index.tssrc/memory/types.ts,导致编译报错或类型推断混乱。
  3. ESM 路径查找报错:由于使用了原生的 ESM (NodeNext),我们在进行多包相对路径引入时偶尔遗漏了 .js 扩展名,导致运行时抛出 ERR_MODULE_NOT_FOUND
  4. Workspace 依赖协议错误:在 package.json 中错误地混用了普通的版本号引用本地包,导致 pnpm 在安装依赖时报 ERR_PNPM_FETCH_404 错误。
  5. CLI 对话回归 Bug:在开发斜杠指令拦截时,误将向 Agent 输送普通文本的 agent.run(userInput) 链路截断,使得 CLI 只能相应特异指令,失去了正常的聊天功能。

工程开发中,“先跑通,再重构” 是正确的,但重构绝不能无限期延后。因此,本章我们专门停下来进行一次系统性的代码重构。


重构原则

在进行本次重构时,我们严守以下四条工程纪律:

  1. 不添加新功能:重构的唯一目标是清理结构,绝不在此时夹带私货编写新逻辑。
  2. 渐进式重构:每次只改动一个模块,改完后立即运行测试脚本 test-phase1.tstest-phase2.ts,确保没有发生功能倒退。
  3. 对外接口(API)保持稳定:确保核心暴露的 AgentMemoryManager 外部签名不变,将影响控制在包内部。

主要重构步骤与代码解析

1. 抽取并统一共享工具方法

我们将所有的 generateId 实现删除,统一挪入本地共享包 @hachimi/shared

packages/shared/src/index.ts 中:

ts
export function generateId(prefix = ""): string {
  return `${prefix}${crypto.randomUUID()}`;
}

在核心包的 packages/core/package.json 中配置引用:

json
"dependencies": {
  "@hachimi/shared": "workspace:*"
}

在核心包的所有需要生成 ID 的地方,统一通过别名路径引入,注意必须携带 .js 尾缀

ts
import { generateId } from "@hachimi/shared"; // 由 tsconfig paths 映射

2. 剥离并去重核心类型定义

我们将 MemoryEntry 从局部的 memory/types.ts 中彻底移出,合并至全局类型中。

更新后的 packages/core/src/types/index.ts

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 中,只保留检索参数定义,并显式引入全局定义:

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 文件:

yaml
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'packages/channels/*'
  - 'packages/tools-builtin/*'
  - 'packages/storage/*'
  - 'apps/*'

同时,检查所有本地子包之间的相互引用,版本号必须强制使用 "workspace:*"

json
"dependencies": {
  "@hachimi/core": "workspace:*",
  "@hachimi/shared": "workspace:*"
}

4. 修复 CLI 的聊天回归缺陷

scripts/chat.ts 中,由于逻辑分支编写不当导致正常对话被跳过。我们重构了 while(true) 对话监听体,建立了清晰的分支拦截链:

ts
// 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 依赖 sharedapps(CLI)位于顶层组装,无循环依赖。
  • 消除编译警告:修复了所有的原生 ESM 相对路径解析,在执行 pnpm typecheck 时顺利通过,零 Warning。

本章总结

通过本章的主动“代码还债”:

  • 我们规范了 Monorepo 的包定义和依赖描述。
  • 提取了核心类型,解决了类型定义重复导致的工程报错。
  • 修复了命令行终端交互的逻辑缺陷,并跑通了完整的回归测试。

重构完成,我们的 Harness 结构变得极为健康。接下来,我们将迈出极为关键的一步——接入真实 LLM