# Agentia 使用说明(给 AI 与人) > 本文件是 **AI 辅助编码的权威入口**,也是人类速查表。 > 仓库根部的 `AGENTS.md` 讲的是「怎么改这个仓库」;**本文件讲的是「怎么用这个框架」**。 包名:`@migor/agentia`(框架)/ `@migor/cli`(命令行)。Node ≥ 18,ESM,TypeScript 7。 **目录** - [0. 心智模型(先读这一段)](#0-心智模型先读这一段) - [1. 最小可运行示例](#1-最小可运行示例) - [2. 项目结构(CLI 约定)](#2-项目结构cli-约定) - [3. 装饰器 spec 字段速查](#3-装饰器-spec-字段速查) - [4. createApp 与 app.run 选项](#4-createapp-与-apprun-选项) - [5. 类型链路(这块决定「编辑器给不给提示」)](#5-类型链路这块决定编辑器给不给提示) - [6. 运行时 API](#6-运行时-api) - [7. 已知边界(如实标注,不要指望框架替你兜)](#7-已知边界如实标注不要指望框架替你兜) - [8. 常见错误](#8-常见错误) --- ## 0. 心智模型(先读这一段) 一次 **run** = 一个 agent 循环跑完一件事,产出一条 **trace**(`traceId === runId`)。 你声明 **四类能力**,它们进同一个「工具菜单」;**主 agent 的模型按 `description` 自己选**: | 能力 | 装饰器 | 谁决定流程 | 典型用途 | |---|---|---|---| | 工具 | `@Tool` | 你的代码(一次调用 = 一个函数) | 确定性操作:查库、算数、调 API | | 技能 | `@Skill` | 你的代码(脚本式,可显式调模型) | 「先取数 → 再让模型写 → 再加工」这种固定流程 | | 子 agent | `@SubAgent` | **模型自己**(独立循环 + 裁剪上下文) | 需要自主多步、且中间过程不该污染主上下文 | | 提示资产 | `@Prompt` | 模型拉取(本质是「按需注入的文本」) | 长文规范/模板,平时不进上下文,需要时拉 | **关键推论**:能力是**运行时**从装饰器注册表收集的,所以 TypeScript 里**没有**「你的能力清单」这种类型 —— 不要写 `app.hello()`。模型通过 `description` 选能力,你通过 `schema` 约束入参。 **同样是一等公民的是可观测**:这条 trace 带每步的 token / 成本 / 耗时 / 错误类型,run 收尾经 `TraceSink` 出口交给你(落库 / 采样 / 脱敏都在缝外,见 §6「观测」)。一句话——**四类能力决定它能做什么,trace 决定你敢不敢上线**。 --- ## 1. 最小可运行示例 ```ts import { Tool, createApp, SystemPrompt } from '@migor/agentia'; const OBJ = { type: 'object', properties: {} } as const; class Greeter { @Tool({ description: '向某人打招呼', schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], }, }) say_hello(input: { name: string }): string { return `你好,${input.name}`; } } const app = createApp({ name: 'greeter', providers: [{ provide: 'greeter', useClass: Greeter }], system: new SystemPrompt().add('role', '你是友好的助手。', true), }); const { result } = await app.run([{ role: 'user', content: '跟小明打个招呼' }]); console.log(result.finalText, result.stopReason, result.trace.totalUsage); ``` 要点: - `@Tool` 方法的**入参必须显式标注类型**(TS 无法从 JSON Schema 反向推断方法形参;`strict` 下不标注会报隐式 any)。 - `schema` 是给**模型**看的契约;方法形参类型是给**你和编译器**看的。想避免两处双写,见第 5 节 `fromZod`。 - 相对 import 必须带 `.js` 后缀(NodeNext)。 --- ## 2. 项目结构(CLI 约定) ```bash npx @migor/cli create my-app # 首次创建:必须带 scope(短名 agentia 在 npm 上是别人的包) cd my-app && npm install # 框架与 CLI 都装进工程 $EDITOR .env # 填 ANTHROPIC_API_KEY(脚手架已生成,且已被 .gitignore 挡住) npm run dev # = agentia dev:本地 inspector 面板(输入 prompt / 选能力 / 选工作目录) npx agentia g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理) npx agentia doctor # 静态体检(未登记/悬空/命名/重复) npx agentia --version # CLI 版本(= -v) ``` > **命令从哪来**:脚手架把 `@migor/cli` 装进工程的 `devDependencies`,所以**工程内**用短名 > `npx agentia …` 即可(走本地 bin:离线可用、版本与工程一同 pin)。**首次创建**必须用带 scope 的 > `npx @migor/cli create` —— npm 上另有一个别人的 `agentia` 包,短名会装错东西。 > 想全局装上(到处都能敲 `agentia`):`npm i -g @migor/cli`。 四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider 数组**。显式注册表在 `src/registry.ts`(`agentia g` 自动维护,也可手改)。 ### 2.1 脚手架生成的文件(**装配与启动是分开的**) | 文件 | 作用 | |---|---| | `src/app.ts` | **装配**:导出 `createAgentApp({ toolSources?, workdir? })` 工厂 + `CAPABILITY_DIRS` + `createSessionStore()`,并在模块顶部 `loadEnvFile()` 读 `.env` | | `src/main.ts` | **启动**:薄入口 —— `createAgentApp()` → `app.run(...)`,再处理 `result.error` | | `src/dev.config.ts` | **数据**(不是逻辑):开发期的声明,如 `multiTurn: ['trip-planner']`;见 §2.2 | | `src/session-store.ts` | `FileSessionStore`:把多轮对话落成 `.agentia/session.json`(原子写);见 §6.4「对话历史」 | | `src/registry.ts` | 显式注册表(`agentia g` 自动维护) | | `src/tools/read-file/` | 示例工具(两个):`list_files` 列**工作目录**的内容(输出首行带工作目录绝对路径),`read_file` 读其中一个文本文件(工作目录由 DI 注入,见 §2.2) | **为什么拆**:`agentia dev` 的调试环要**复用同一个工厂**,才能把「这次调哪个能力 / 工作目录是哪个」 喂进 `createApp`。所以装配必须在 `app.ts` 里、以**函数**形态存在 —— 别把 `createApp(...)` 搬回 `main.ts`(搬回去 dev 环就起不来了)。 > **脚手架 `src/app.ts` 怎么找这些目录**:按**本文件位置**解析(`fileURLToPath(new URL('tools/', import.meta.url))`), > 所以 dev 解析到 `src/`、`npm run build` 之后解析到 `dist/` —— 从任何目录启动都成立,也不受 cwd 影响。 > 别改成 cwd 相对写法(形如 `src/tools` 的字符串):那样 `node dist/main.js` 会去加载 `src/` 下的 `.ts` > 源码,而装饰器不是可擦除的类型语法,Node 直接跑不了。空分类目录(还没有该类型的能力 ⇒ 构建后没有 > 对应 `dist/<分类>/`)要过滤掉,否则 `discover` 会因「显式给出的路径不存在」而报错。 > **进程外资源的所有权**(`app.ts` 里最容易写错的一处):MCP 连接器、数据库句柄、定时器这类 > **进程外资源一律在模块作用域创建、再以 `useValue` 注入**,不许在 provider 的构造函数里建。 > 理由见 §2.2 的重启策略:框架没有 `AgentApp.close()`,构造函数里建的东西会随每次重建攒下孤儿进程。 ### 2.2 开发期调试环(`agentia dev`) `npm run dev` 起一个**本地 inspector 面板**,四组输入都在面板上(不用改代码重跑)。 另有两个操作按钮(都不是「装饰」—— 缺了各自的后果见下面两段):**■ 中止**(只在有 run 在飞时出现) 与**清空对话**(只在对话视图出现时显示): | 输入 | 口径 | |---|---| | **prompt** | 输入框 + `↑`/`↓` 调回历史(像 shell)。面板上「点一下 = 一次**真** run」 | | **能力** | 多选,缺省全选。收窄的是**菜单**(主 agent 仍在环里),不是「只可能调它」—— 别的能力 `tools` 里的显式引用仍能调到被排除的那个 | | **工作目录** | 目录选择器(`浏览…`)+ 「系统选择…」原生对话框。**它是这个环的主控件**:任务型 agent 的 prompt 几乎不变,变的是目录。三个点击语义:点**目录名**进去 / 点 `..` 上一层 / **点文件名** = 把它**所在的**目录当工作目录(prompt 为空时顺手把文件名填进去,你写好的话一个字不动);`.git` 这类**隐藏目录**单列在选择器末尾(可达但靠后)。`浏览…` **总是**按输入框里的路径打开 —— 敲了路径再点它就是打开那个路径(不是「再点一次关掉」);点面板外面收起。`系统选择…` 拉 **OS 原生文件夹对话框**(由 CLI 本机进程弹出,因为浏览器拿不到所选目录的绝对路径),选完直接填进输入框。⚠️ 「换了目录」要让**模型**感知到,靠脚手架 `read-file` 能力里的 `list_files`:它输出首行就是工作目录绝对路径 —— 没有这类工具时模型不知道自己在哪、只能瞎猜文件名(行为看起来「没变」) | | **多轮** | 跟能力走:`src/dev.config.ts` 里 `multiTurn: [...]` 声明哪些能力该多轮,混选时取 **OR** 且面板**标出来源**(`多轮·trip-planner`) | `npm run dev -- "你的问题"` 可以直接把第一句 prompt 带上(等价于启动后在输入框里敲)。 **右栏是实时的(在飞就看得到)**:run 一开跑,右栏就从空树开始,**span 一个个长出来** (`◌` = 还没结束,`●` = 已结束),收尾时用**整棵 trace** 覆盖一遍。实现走框架的增量记账出口 (`onTraceEvent`,0.8.3):runner 订阅 → 逐笔 `POST /ingest-event` → 父进程广播 → 面板按 `seq` 折回。 两条边界(与框架同名出口一致,都写在文档里以免被当成缺陷): - **不保证送达**:SSE 断了就断了,没有重放。所以「在飞的那棵树」是**临时的** —— 刷新页面之后 早先那些 span 不会回来(收尾的整棵 trace 会补齐它们),**收尾那份永远覆盖在飞的这份**。 - 它只影响「此刻看到什么」,**不影响 run 本身**,也不影响落盘的 trace(那是另一条缝)。 **模型正文按 Markdown 渲染**(最终回复 + 对话视图的助手轮):标题 / 粗斜体 / 行内码 / 围栏码 / 列表 / 引用 / 分隔线 / 链接。解析器是本仓自己写的(零依赖),**只产出 token 树、不碰 HTML** —— 模型输出里的 `