npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

knight-agent

v1.4.9

Published

Knight-agent智能体架构

Readme

Knight Agent — 架构使用手册

目录


1. 架构概览

┌──────────────────────────────────────────────────┐
│                   AbstractLLM                      │
│  ┌──────────┐  ┌──────────┐  ┌────────────────┐  │
│  │  config  │  │ messages │  │   sending      │  │
│  │ (Config) │  │(Message) │  │   (lock)       │  │
│  └──────────┘  └────┬─────┘  └────────────────┘  │
│       │              │                            │
│       │    ┌─────────┴──────────┐                 │
│       │    │  MessageData[]     │                 │
│       │    │  OnTrim/OnCompact  │                 │
│       │    └────────────────────┘                 │
│       │                                           │
│  ┌────▼──────────────────────────────────────┐    │
│  │  OnSend(prompt, tools?, onChunk?)          │    │
│  │    → OnAddUserMessage                      │    │
│  │    → OnCompactMessages                     │    │
│  │    → messages.OnTrim                       │    │
│  │    → OnRequest (HTTP)                      │    │
│  │    → OnHandleResponse (Tool Call Loop)     │    │
│  └────────────────────────────────────────────┘    │
└─────────────────────┬────────────────────────────┘
                      │
       ┌──────────────┼──────────────────┐
       │              │                  │
  Claude  DeepSeek  Gemini  OpenAI  QWen  Ollama

核心流程: OnSend(prompt) → 消息预处理 → HTTP 请求 → 响应解析 → 工具调用循环

继承体系: AbstractObjectAbstractLLM<T>Claude / DeepSeek / Gemini / Ollama / OpenAI / QWen


2. 快速开始

import { AbstractLLM, LLMUtility } from "knight-agent";

// 1. 创建 LLM 实例(系统提示词通过 Config 传入,构造时自动消费)
const llm = AbstractLLM.OnCreat({
    type: "DeepSeek",
    model: "deepseek-chat",
    apiKey: "sk-your-api-key",
    stream: true,
    systemPrompt: "你是一个有帮助的助手。",
});

// 2. 发送消息
const response = await llm.OnSend("你好,介绍一下你自己");
console.log(response.content);
console.log(response.usage);  // { input: 50, output: 30, total: 80 }

3. 核心概念

3.1 AbstractLLM(抽象大模型)

框架核心基类,封装了消息管理、请求发送、工具调用循环、配置校验等通用逻辑。各平台通过继承并实现抽象方法接入。

abstract class AbstractLLM<T extends Config> extends AbstractObject {
    // ===== 静态成员 =====
    static OnCreat(config, data?): AbstractLLM;        // 工厂创建
    static OnCheckConfig(config): CheckResult;          // 配置校验

    // ===== 实例成员 =====
    protected config: T;                                // 当前配置
    protected readonly messages: Message;               // 消息缓存

    // 配置
    OnSetConfig(config): CheckResult;                   // 设置/更新配置
    OnCheckPlatfrom(config): CheckResult;              // 检测平台类型是否变更

    // 对话
    OnSend(prompt, tools?, onChunk?): Promise<Response>;   // 发送消息

    // 工具
    OnExecuteTool(toolCall, tool): Promise<ToolResult>;     // 手动执行工具

    // 生命周期
    OnImport(data): void;                               // 导入数据
    OnExport(): MessageData[];                          // 导出消息列表
    OnDestroy(): boolean;                               // 销毁

    // 抽象方法(派生类必须实现)
    protected abstract OnBuildBody(
        messages: MessageData[],
        tools: ToolData[],
        stream: boolean
    ): Record<string, unknown>;                         // 构建平台 API 请求体
    protected abstract OnRequest(
        tools: ToolData[],
        onChunk?: StreamCallback
    ): Promise<Response>;                               // 发起 HTTP 请求
    protected abstract OnStopReason(
        reason: string
    ): FinishReason;                                    // 平台停止原因映射
}

OnSend 内部流程:

  1. sending 锁检查(防重入)
  2. 参数校验(config 为空 / prompt 为空 → 返回错误 Response)
  3. OnAddUserMessage(prompt) — 添加用户消息
  4. OnCompactMessages() — 截断过长工具输出(基类实现)
  5. messages.OnTrim() — Token + 轮次混合裁剪
  6. OnBuildBody(messages, tools, stream) — 构建平台请求体,将统一 MessageData[] 映射为平台 API wire format
  7. OnRequest(tools, onChunk) — 发起 HTTP 请求
  8. OnAddAssistantMessage(content, toolCalls, result.usage, result.thinkingContent) — 添加助手消息(usage 由 Message.OnAdd 自动累加,thinkingContent 持久化到 MessageData)
  9. OnHandleResponse(...) — 工具调用循环(最多 100 轮)

3.2 Message(消息缓存)

统一管理 LLM 上下文消息,系统消息始终存储在 messages[0](永不裁剪):

class Message {
    constructor(maxCache?: number);             // 默认 8000 tokens

    OnSetSystem(message: MessageData): void;    // 设置/替换系统消息
    OnGetSystem(): MessageData | undefined;     // 获取系统消息
    OnAdd(message: MessageData): this;          // 添加单条消息
    OnAddRange(list: MessageData[]): this;      // 批量添加
    OnGetAll(): MessageData[];                  // 获取全部消息(含系统消息的副本)
    OnGetLast(): MessageData[];                 // 获取最后一轮消息
    OnGetCount(): number;                       // 消息总数
    OnGetRoundCount(): number;                  // 当前轮次数(role==="user" 的消息数)
    OnGetMaxRounds(): number;                   // 最大轮次容量(内部自动计算 = maxCache / 80)
    OnGetRoundPercent(): number;                // 轮次利用率 0~1

    OnSetMaxCache(maxTokens): void;             // 设置 Token 上限(同步更新 maxRounds)
    OnGetTokenCount(): number;                  // Token 估算(基于消息内容字符数)
    OnGetTokenPercent(): number;                // Token 利用率 0~1
    OnGetRoundUsage(): Usage;                   // 本轮累计 Token 用量(计费)
    OnTrim(): number;                           // 混合裁剪(Token 硬限制 + 轮次软限制)
    OnCompact(transform): number;               // 逐条转换(工具输出截断等)
    OnRestore(data: MessageData[]): void;       // 从快照恢复
    OnClear(): void;                            // 清空
}

系统消息模型:

  • 系统消息始终存储在 messages[0],角色为 "system"
  • OnTrim() 跳过系统消息(从 index 1 开始),永不裁剪
  • OnGetAll() 返回全部消息(含系统消息)
  • 各平台在 OnBuildBody() 中通过 OnGetSystem() 获取并按需转换:
    • 数组型(Ollama/OpenAI): 系统消息已在数组中,直接发送
    • 注入型(DeepSeek/QWen): 提取后与 JSONSchema prompt 合并,替换回数组首位
    • 字段型(Claude/Gemini): 提取后放入请求体的独立字段 (system / systemInstruction)

4. LLM 平台配置

4.1 通用配置字段

所有平台配置均继承自 LLMUtility.Config

| 字段 | 类型 | 默认值 | 说明 | |---|---|---|---| | type | string | — | 平台标识:"Claude" / "DeepSeek" / "Gemini" / "OpenAI" / "QWen" / "Ollama" | | url | string | 平台默认 | API 端点 | | model | string | 平台默认 | 模型名称 | | apiKey | string | — | API 密钥(必填) | | timeout | number | 60000 | 请求超时 (ms) | | temperature | number | 1 | 采样温度 (0~2) | | topP | number | 1 | 核采样 (0~1) | | maxTokens | number | 4096 | 最大输出 token 数 | | stop | string \| string[] | — | 停止序列(数组最多 4 条) | | stream | boolean | false | 流式响应开关 | | seed | number | — | 可复现采样种子(须为整数) | | systemPrompt | string | — | 系统提示词(值变更时自动更新) | | maxCache | number | 8000 | 最大 Token 缓存上限(超出自动裁剪) | | maxToolOutput | number | 800 | 工具输出截断长度 | | responseFormat | string \| Record<string, any> | "text" | 输出格式:字符串为模式名("text"/"json_object"/"json_schema"),对象为结构化输出约束(任意 JSON Schema 结构),各平台自动映射 |

4.2 配置校验

AbstractLLM.OnCheckConfig(config) 提供逐字段细化校验:

const result = AbstractLLM.OnCheckConfig({
    type: "DeepSeek",
    model: "deepseek-chat",
    apiKey: "sk-xxx",
});
// { valid: true }

const bad = AbstractLLM.OnCheckConfig({
    type: "",
    model: "deepseek-chat",
    apiKey: "",
});
// { valid: false, error: "config.type is required" }

校验规则:

  • 必填字段:typemodel 必须非空
  • type 必须已注册到 Component 命名空间
  • url 若提供,须为 http://https:// 开头
  • 数值范围:temperature (0~2)、topP (0~1)、maxTokens/timeout/maxCache/maxToolOutput (> 0)
  • seed 须为整数
  • stop 数组最多 4 条

4.3 配置热更新

OnSetConfig 可在任意时机调用,配置未变更时零开销返回,配置变更时合并新旧配置(保留平台默认值):

// 首次设置
llm.OnSetConfig({ type: "DeepSeek", model: "deepseek-chat", apiKey: "sk-aaa" });

// 仅更新 apiKey,url/model 等保留原有值
llm.OnSetConfig({ apiKey: "sk-bbb" });

// 相同配置再次调用 — 无操作
llm.OnSetConfig({ type: "DeepSeek", model: "deepseek-chat", apiKey: "sk-bbb" });
// → { valid: true },不做任何更新

原理: 配置合并使用 { ...this.config, ...config } 确保平台默认值不丢失。平台类型通过 OnCheckPlatfrom() 检测,构造阶段(this.config 未初始化)放行,热更新阶段类型不匹配则返回错误。

4.4 DeepSeek

interface DeepSeekConfig extends Config {
    type: "DeepSeek";
    thinking?: { type: "enabled" | "disabled" };
    userId?: string;
}
// 默认: deepseek-v4-flash | https://api.deepseek.com/chat/completions
// 鉴权: Authorization: Bearer <apiKey>
// 思考模式: thinking: { type: "enabled" } → response.thinkingContent
// 结构化输出: API 仅支持 json_object,传入对象时自动 JSON.stringify 注入 system prompt (见 4.10)

4.5 OpenAI

interface OpenAIConfig extends Config {
    type: "OpenAI";
    reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
    responseFormat?: "text" | "json_object" | "json_schema";
    jsonSchema?: { name: string; schema: Record<string, any>; strict?: boolean };  // 配合 "json_schema" 使用,也可直接通过基类 responseFormat 传入对象
    streamOptions?: { include_usage?: boolean };
    modalities?: Array<"text" | "audio">;
    store?: boolean;
    serviceTier?: "auto" | "default" | "flex" | "priority";
    safetyIdentifier?: string;
    webSearchOptions?: { searchContextSize?: "low" | "medium" | "high" };
    parallelToolCalls?: boolean;
}
// 默认: gpt-4o | https://api.openai.com/v1/chat/completions
// 鉴权: Authorization: Bearer <apiKey>
// 思考模式: reasoningEffort + 支持推理的模型 → response.thinkingContent

4.6 Claude

interface ClaudeConfig extends Config {
    type: "Claude";
    thinking?: { type: "enabled" | "disabled" | "adaptive"; budgetTokens?: number };
    outputConfig?: { effort?: "low" | "medium" | "high" | "xhigh" | "max" };
    speed?: "standard" | "fast";
    userId?: string;
}
// 默认: claude-sonnet-4-6 | https://api.anthropic.com/v1/messages
// 鉴权: x-api-key: <apiKey> + anthropic-version: 2023-06-01
// 注意: Opus 4.7+ 中 temperature/topP/topK 已弃用,仅接受 1.0
// 思考模式: thinking: { type: "enabled" } → response.thinkingContent
// 结构化输出: 原生支持 output_config.format,传入对象时自动映射 (见 4.10)

4.7 Gemini

interface GeminiConfig extends Config {
    type: "Gemini";
    topK?: number;
    candidateCount?: number;
    thinkingConfig?: { includeThoughts?: boolean; thinkingBudget?: number };
    safetySettings?: Array<{ category: string; threshold: string }>;
    responseModalities?: Array<"TEXT" | "IMAGE">;
}
// 默认: gemini-2.5-flash | ...googleapis.com/v1beta/models/{model}:generateContent
// 鉴权: ?key=<apiKey> 查询参数 | 流式: ?alt=sse&key=<apiKey>
// URL 中的 {model} 由 OnResolveUrl() 在请求时动态替换
// 思考模式: thinkingConfig: { includeThoughts: true } → response.thinkingContent
// 结构化输出: 原生支持 responseJsonSchema,传入对象时自动映射 (见 4.10)

4.8 QWen(通义千问)

interface QWenConfig extends Config {
    type: "QWen";
    enableSearch?: boolean;
    vlHighResolutionImages?: boolean;
    repetitionPenalty?: number;
    thinking?: { type: "enabled" | "disabled" };
}
// 默认: qwen-plus | https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
// 鉴权: Authorization: Bearer <apiKey>
// 思考模式: thinking: { type: "enabled" } → response.thinkingContent
// 结构化输出: API 仅支持 json_object 模式,传入对象时自动 JSON.stringify 注入 system prompt 并设置 response_format: { type: "json_object" }

4.9 Ollama

interface OllamaConfig extends Config {
    type: "Ollama";
    keep_alive?: number | string;
    think?: boolean | "high" | "medium" | "low" | "max";
    options?: Record<string, unknown>;
}
// 默认: llama3.2 | http://localhost:11434/api/chat
// 无需 apiKey | 流式使用 NDJSON 格式(非 SSE)
// 思考模式: think: true (或 "high"/"medium"/"low"/"max") → response.thinkingContent
// 结构化输出: format 参数原生支持 JSON Schema 对象,传入对象时直接透传

4.10 结构化输出 (responseFormat)

responseFormat 接受 string | Record<string, any>,用户可传入字符串模式名或任意 JSON Schema 对象,各平台自动映射为各自的原生参数:

const llm = AbstractLLM.OnCreat({
    type: "OpenAI",
    model: "gpt-4o",
    apiKey: "...",
    responseFormat: {
        type: "object",
        properties: {
            name: { type: "string", description: "姓名" },
            age:  { type: "number", description: "年龄" }
        },
        required: ["name", "age"]
    }
});

| 平台 | 映射方式 | 机制 | |---|---|---| | OpenAI | response_format: { type: "json_schema", json_schema: { name: "response", strict: true, schema } } | 原生 constrained decoding | | Claude | output_config: { format: { type: "json_schema", schema } } | 原生 constrained decoding (2026.2 GA) | | Gemini | generationConfig: { responseMimeType: "application/json", responseJsonSchema: schema } | 原生 constrained decoding | | Ollama | format: schema | 原生 JSON Schema 透传 | | DeepSeek | response_format: { type: "json_object" } + JSON.stringify(schema) 注入 system prompt | API 不支持 json_schema,将 schema 对象序列化后注入提示词引导模型 | | QWen | response_format: { type: "json_object" } + JSON.stringify(schema) 注入 system prompt | API 不支持 json_schema,将 schema 对象序列化后注入提示词引导模型 |

DeepSeek/QWen 降级策略: 这两个平台 API 不支持原生 JSON Schema 约束。框架将传入的对象通过 JSON.stringify 序列化后拼入 system prompt,同时设置 response_format: { type: "json_object" }。用户无需关心平台差异。

注意: OpenAI 同时保留了 jsonSchema 扩展字段({ name, schema, strict? }),传入字符串 "json_schema" 时可配合使用,用于需要自定义 name 或关闭 strict 的场景。


5. 系统提示词

系统提示词通过 Config.systemPrompt 传入,构造时自动消费。后续 OnSetConfig 传入不同值时也会更新:

const llm = AbstractLLM.OnCreat({
    type: "DeepSeek",
    model: "deepseek-chat",
    apiKey: "sk-xxx",
    systemPrompt: "你是一个资深的后端架构师,使用 TypeScript 回答。",
});

// 更新系统提示词(仅当与缓存中不同时生效)
llm.OnSetConfig({ systemPrompt: "你现在是一个前端专家。" });

内部流程:

  1. 构造函数或 OnSetConfig 合并配置 → 比较 config.systemPrompt 与缓存中的系统消息
  2. 若不同 → 调用 this.OnAddSystemMessage(text) 替换 messages[0]
  3. 系统消息始终作为 messages[0] 持久化,Trim 不裁剪
  4. 请求时各平台 OnBuildBody() 通过 this.messages.OnGetSystem() 获取并按需转换

6. 工具定义与调用

工具符合 MCP InputSchema 规范,LLM 在合适场景下通过 tool_calls 调用:

// 定义工具
const tools: LLMUtility.ToolData[] = [{
    name: "calculator",
    description: "执行四则运算",
    inputSchema: {
        type: "object",
        properties: {
            expression: { type: "string", description: "数学表达式" },
        },
        required: ["expression"],
    },
    handler: async (args) => {
        const expr = String(args.expression);
        const result = Function(`"use strict"; return (${expr})`)();
        return JSON.stringify({ expression: expr, result });
    },
}];

// 传入 OnSend,框架自动处理工具调用循环
const response = await llm.OnSend("计算 3 + 5 * 2", tools);
console.log(response.content);  // 含工具调用结果的最终回复

工具入参校验: 框架在调用 handler 前会使用 inputSchema 校验 toolCall.arguments,发现 required 缺失、类型不符、枚举越界等会直接返回错误给 LLM 促使其纠正。


7. 对话请求

7.1 非流式对话

const response = await llm.OnSend("你好");
console.log(response.content);          // 完整回复文本
console.log(response.thinkingContent);  // 思考/推理内容(需平台支持 thinking mode)
console.log(response.finishReason);     // "stop" | "length" | "tool_calls" | "content_filter" | "unknown"
console.log(response.usage);            // { input: 50, output: 30, total: 80 }
console.log(response.toolCalls);        // 工具调用列表 (如有)

7.2 流式对话

const response = await llm.OnSend("讲一个故事", null, (chunk) => {
    if (chunk.content) {
        process.stdout.write(chunk.content);   // 实时增量文本
    }
    if (chunk.thinkingContent) {
        // 思考增量 — 与 content 互斥,同一 chunk 不同时包含两者
        process.stderr.write(chunk.thinkingContent);
    }
    if (chunk.finishReason) {
        console.log("\n结束:", chunk.finishReason);
    }
});
// response.content 为完整文本,response.thinkingContent 为完整思考内容
// response.usage 为最终计费用量

7.3 工具调用流程

当 LLM 返回 finishReason: "tool_calls" 时,框架自动进入工具调用循环:

User Prompt → OnRequest → Response (finishReason="tool_calls")
    → OnExecuteTool(toolCall1, tool) → OnAddToolMessage(result1)
    → OnExecuteTool(toolCall2, tool) → OnAddToolMessage(result2)
    → OnRequest (含工具结果) → Response (finishReason="stop")

循环上限 100 轮,防止死循环。工具调用中间的流式回调不会触发,避免干扰最终输出。

也可手动执行工具:

const response = await llm.OnSend("计算 15.7 * 3.2");
for (const tc of response.toolCalls || []) {
    const tool = tools.find(t => t.name === tc.name);
    const result = await llm.OnExecuteTool(tc, tool);
    console.log(`${tc.name} → ${result.data}`);
}

7.4 思考模式 (Thinking)

部分平台支持"思考模式"(Thinking/Reasoning),模型在给出最终回复前会生成内部推理过程。框架将思考内容提取到 response.thinkingContent,并与 content 分离存储。

平台启用方式

| 平台 | 配置字段 | 示例 | |---|---|---| | DeepSeek | thinking: { type: "enabled" } | 默认关闭 | | OpenAI | reasoningEffort: "medium" | 需支持推理的模型(如 o-series) | | Claude | thinking: { type: "enabled", budgetTokens: 4000 } | 支持 "enabled" / "disabled" / "adaptive" | | Gemini | thinkingConfig: { includeThoughts: true } | 可指定 thinkingBudget 控制 token 预算 | | QWen | thinking: { type: "enabled" } | 默认 { type: "disabled" } | | Ollama | think: true"high" / "medium" / "low" / "max" | 需模型支持(如 qwen3 系列) |

同步获取

const llm = AbstractLLM.OnCreat({
    type: "DeepSeek",
    model: "deepseek-chat",
    apiKey: "sk-xxx",
    thinking: { type: "enabled" },
});

const r = await llm.OnSend("13.8 和 13.11 哪个大?请推理后给出答案。");
console.log(r.content);           // 正式回复
console.log(r.thinkingContent);   // 模型内部推理过程(可能为空)

流式获取

let thinking = "";
let text = "";

const r = await llm.OnSend("推导勾股定理", null, (chunk) => {
    if (chunk.thinkingContent) thinking += chunk.thinkingContent;
    if (chunk.content) text += chunk.content;
});

// thinking === r.thinkingContent  — 流式拼接与最终响应一致
// text === r.content

contentthinkingContent 互斥:流式回调中,同一 StreamChunk 不同时包含 contentthinkingContent。模型先输出思考内容,再输出正式回复。

多轮持久化

thinkingContent 会自动写入 MessageData 并在后续请求中回填到平台 API 消息中,确保多轮对话时模型能"看到"之前的推理上下文。跨平台迁移时 thinkingContent 同样会被保留。

注意:Claude 的 thinking 回填需要 signature 字段,当前版本暂不支持。其余平台(DeepSeek/OpenAI/QWen/Gemini/Ollama)均完整支持 thinking 的提取、流式输出和多轮回填。


8. 消息缓存与轮次管理

消息缓存

Message 负责管理 LLM 上下文消息。每次 OnSend 前自动执行:

  1. OnCompactMessages() — 截断过长的工具输出(默认 > 800 字符),由基类统一实现
  2. messages.OnTrim() — 混合裁剪(Token 硬限制 + 轮次软限制)

Token 容量与剔除策略

maxCache 语义为 Token 容量上限(默认 8000)。Message 内部自动计算 maxRoundsmaxCache / 80,最小 10)作为轮次辅助限制。

裁剪规则:

  1. Token 硬限制:当 OnGetTokenCount() > maxCache * 0.8 时,删除最旧完整轮次直至安全水位
  2. 轮次软限制:当 OnGetRoundCount() > maxRounds 时,删除最旧完整轮次

maxCache <= 0 时不限制。系统消息 messages[0] 永不裁剪,剔除单元为完整一轮。

Token 计数

OnGetTokenCount() 基于消息内容字符数估算(~3.5 字符/token),不依赖 API 返回的 usage 数据:

public OnGetTokenCount(): number {
    let total = 0;
    for (const msg of this.messages) {
        total += Math.ceil((msg.content?.length ?? 0) / 3.5);
        if (msg.toolCalls?.length) {
            total += Math.ceil(JSON.stringify(msg.toolCalls).length / 3.5);
        }
        if (msg.thinkingContent?.length) {
            total += Math.ceil(msg.thinkingContent.length / 3.5);
        }
    }
    return total;
}

设计说明:API 返回的 usage 是每次 HTTP 请求的独立用量(工具调用循环中各请求的 prompt 有大量重叠),适合计费但不适合缓存大小估算。OnGetTokenCount() 使用字符估算保持一致性。

Token 用量追踪(计费)

API usage 数据由 Message.OnAdd 自动累加并 stamp 到每条 assistant 消息上。Response.usageMessage.OnGetRoundUsage() 填充,即为该轮累计计费用量,外部直接累加即可:

let totalBilling = 0;

const r1 = await llm.OnSend("第一问");
totalBilling += r1.usage.total;  // 该轮计费用量

const r2 = await llm.OnSend("第二问");
totalBilling += r2.usage.total;

注意:不要从 messages 遍历求和所有 assistant 的 usage——同一轮内的中间 assistant 持有部分快照,会重复计数。Response.usage 已处理此问题。


9. 状态导出与还原

支持完整的 LLM 状态快照,用于会话持久化。导出/导入直接使用 MessageData[]

// 导出 — 返回全部消息列表
const snapshot: LLMUtility.MessageData[] = llm.OnExport();
console.log(`导出: ${snapshot.length} 条消息`);

// 还原 — 创建时传入快照
const restored = AbstractLLM.OnCreat(
    { type: "DeepSeek", model: "deepseek-chat", apiKey: "sk-xxx" },
    snapshot,
);

// 或分步导入
const llm2 = AbstractLLM.OnCreat({ type: "DeepSeek", model: "...", apiKey: "..." });
llm2.OnImport(snapshot);

// 继续对话
await restored.OnSend("刚才我们聊到哪了?");

快照优先级: OnImport(data) 传入的快照代表精确的对话状态,会完全替换当前消息缓存。若快照中不含系统消息,不会自动从 Config.systemPrompt 补充。如需在还原时添加系统提示词,请在快照 messages[0] 手动插入一条 { role: "system", content: "...", timestamp: Date.now() } 消息。


10. 并发控制

同一实例同一时间只能有一个 OnSend 在执行,并发调用会立即返回错误:

const [a, b] = await Promise.all([
    llm.OnSend("请求A"),
    llm.OnSend("请求B"),
]);
// a.content === "LLM is busy, a previous OnSend() is still in progress"
// b 正常返回

解决方案: 创建多个实例:

const llm1 = AbstractLLM.OnCreat({ type: "DeepSeek", model: "...", apiKey: "sk-aaa" });
const llm2 = AbstractLLM.OnCreat({ type: "DeepSeek", model: "...", apiKey: "sk-bbb" });

const [r1, r2] = await Promise.all([
    llm1.OnSend("问题1"),
    llm2.OnSend("问题2"),
]);

11. 工具参考

11.1 LLMUtility

核心类型命名空间:

配置类型: | 导出 | 说明 | |---|---| | Config | 通用 LLM 配置接口 | | CheckResult | { valid: boolean, error?: string } |

消息类型: | 导出 | 说明 | |---|---| | MessageData | 统一消息接口 (role, content, timestamp, toolCalls?, toolCallId?, name?, usage?, thinkingContent?) | | Message | 消息缓存类(管理 MessageData[],含轮次裁剪与消息压缩) | | Response | 对话响应(平台 OnRequest 返回不含 messages,OnSend 补充 messages 和累计 usage) |

工具类型: | 导出 | 说明 | |---|---| | ToolData | 工具定义 (name, description, inputSchema, handler) | | ToolCall | LLM 返回的工具调用 (id, name, arguments) | | ToolResult | 工具执行结果 (id, name, data) | | JSONSchema | 工具入参规范(符合 MCP 规范) |

响应类型: | 导出 | 说明 | |---|---| | Response | 统一响应 (id, model, content, thinkingContent?, toolCalls?, finishReason, usage, raw, messages?) | | StreamChunk | 流式分块 (content, thinkingContent? 互斥, finishReason?) | | StreamCallback | (chunk: StreamChunk) => void | | Usage | Token 用量 (input, output, total) | | FinishReason | "stop" \| "length" \| "tool_calls" \| "content_filter" \| "unknown" |

11.2 NetworkUtility

// HTTP 请求
const res = await NetworkUtility.HTTP.OnRequest<T>(url, {
    method: "post",
    data: body,
    headers: { "Content-Type": "application/json" },
    timeout: 30000,
});
// res.data: T | res.status | res.statusText | res.ok

// SSE 流式请求
for await (const line of NetworkUtility.HTTP.OnStreamRequest(url, options)) {
    // line 是单行 JSON 字符串
}

// HttpError
try { ... } catch (e) {
    if (e instanceof NetworkUtility.HTTP.HttpError) {
        console.log(e.status, e.statusText, e.data);
    }
}

// WebSocket
const ws = new NetworkUtility.Websocket();
ws.On("open", () => console.log("connected"));
ws.On("message", (ws, event) => console.log(event.data));
ws.OnConnect("wss://example.com/ws");
ws.OnSend(JSON.stringify({ type: "ping" }));
ws.OnDisConnect();

11.3 StringUtility

| 函数 | 说明 | |---|---| | empty(str): boolean | 判空(null / undefined / 纯空白) | | GetHash(key): number | 字符串哈希值 | | GetGUID(): string | UUID v4 |

11.4 LogUtility

| 函数 | 说明 | |---|---| | SetSwitch(value: boolean) | 全局日志开关 | | LogTip(...msgs) | 提示(绿色) | | LogInfo(...msgs) | 信息(蓝色) | | LogWarning(...msgs) | 警告(橙色) | | LogError(...msgs) | 错误(红色) | | LogSystem(...msgs) | 系统(紫色) |

前缀格式: [KNIGHT][类型标签] 消息内容

11.5 ObjectUtility

// 数组池
new ObjectUtility.ArrayPool<MyType>();
// Add / Remove / Get / Exist

// 键值池
new ObjectUtility.MapPool<MyType>();
// Add / Remove / Get / Set / KeysToArray / ValuesToArray

// 队列
new ObjectUtility.QueuePool<MyType>();
// Enqueue / Dequeue / Peek

// 反射
ObjectUtility.OnGetFunctionSignature(fn);
// → { functionName, parameters: [{ name, type?, defaultValue?, optional? }] }

// 类型判断
ObjectUtility.IsNonEmptyObject(value);
// → true 当 value 是非空普通对象(排除 null、数组、空对象)

11.6 InterfaceUtility

type IType<T> = new (...args: any[]) => T;           // 构造函数类型约束
type IAction<T, K, M, N, Q, U, V, W>;               // 泛型回调
type IEvent<T, K, M, N, Q, U, V, W>;

11.7 TimeUtility / PromiseUtility

TimeUtility.Now();          // Date.now()
TimeUtility.NowString();    // "2026-07-15 14:30:00"
TimeUtility.NowHEX(16);     // Date.now().toString(16)

PromiseUtility.Wait(1000);  // await 1 秒

12. 高级用法

12.1 扩展自定义 LLM

import { AbstractLLM, RegisterLLM, LLMUtility, NetworkUtility } from "knight-agent";

// 1. 定义平台类型
namespace MyLLMType {
    export interface Config extends LLMUtility.Config { type: "MyLLM"; customOption?: string; }
    export interface ChatResponse { /* ... */ }
}

// 2. 实现 — 需覆写 OnBuildBody、OnStopReason 和 OnRequest;OnSend/OnAdd*/OnCompactMessages 由基类提供
@RegisterLLM("MyLLM")
class MyLLM extends AbstractLLM<MyLLMType.Config> {
    public constructor(config?: MyLLMType.Config) {
        super({ type: "MyLLM", url: "https://api.myllm.com/chat", model: "default", apiKey: "", timeout: 60000, ...config });
    }

    protected OnBuildBody(messages: LLMUtility.MessageData[], tools: LLMUtility.ToolData[], stream: boolean): Record<string, unknown> {
        // 将统一 MessageData[] 映射为平台 API wire format
        return {
            model: this.config.model,
            messages: messages.map(m => ({ role: m.role, content: m.content })),
            // ... 平台特定字段
        };
    }

    protected OnStopReason(reason: string): LLMUtility.FinishReason {
        switch (reason) { case "stop": return "stop"; default: return "unknown"; }
    }

    protected async OnRequest(tools: LLMUtility.ToolData[], onChunk?: LLMUtility.StreamCallback): Promise<LLMUtility.Response> {
        const body = this.OnBuildBody(this.messages.OnGetAll(), tools, false);
        const response = await NetworkUtility.HTTP.OnRequest<MyLLMType.ChatResponse>(this.config.url, {
            method: "post",
            data: body,
            headers: { "Authorization": `Bearer ${this.config.apiKey}`, "Content-Type": "application/json" },
            timeout: this.config.timeout,
        });
        // 将平台响应映射为 LLMUtility.Response ...
    }
}

要点: 构造函数必须将用户 config 展开在平台默认值之后({ ...defaults, ...config }),确保默认值不被覆盖。OnSend 主流程、OnAdd* 消息管理和 OnCompactMessages 工具输出截断均由基类提供,无需覆写。仅需覆写三个抽象方法:OnBuildBody(统一消息 → 平台 API wire format 映射)、OnStopReason(停止原因码映射)和 OnRequest(HTTP 请求与响应解析)。

12.2 实例管理与并发

// 静态池管理
AbstractLLM.OnCreat(config1);
AbstractLLM.OnCreat(config2);
// 两个实例均在 AbstractLLM.llms 中

// 多实例并发
const [r1, r2] = await Promise.all([
    llm1.OnSend("问题1"),
    llm2.OnSend("问题2"),
]);

// 销毁
llm1.OnDestroy();  // 从池中移除,清理缓存

12.3 错误处理

// OnSend 在以下情况返回错误 Response(不抛异常):
// - config 为空: content = "LLM config is not initialized..."
// - prompt 为空: content = "Prompt is empty or null"
// - 并发冲突:    content = "LLM is busy..."

// OnSetConfig 返回 CheckResult:
const result = llm.OnSetConfig(config);
if (!result.valid) {
    console.error("配置错误:", result.error);
}

// 工具入参校验错误会返回:
// { id: toolCall.id, name: toolCall.name, data: '{"error":"missing required argument \\"query\\""}' }

// HTTP / 网络错误会抛出异常:
try { await llm.OnSend("hello"); } catch (e) {
    if (e instanceof NetworkUtility.HTTP.HttpError) {
        // HTTP 层错误: e.status, e.statusText, e.data
    }
}

13. 完整示例

import { AbstractLLM, LLMUtility, LogUtility } from "knight-agent";

async function main() {
    // ===== 1. 创建实例 =====
    const llm = AbstractLLM.OnCreat({
        type: "DeepSeek",
        model: "deepseek-chat",
        apiKey: process.env.DEEPSEEK_API_KEY!,
        stream: true,
        temperature: 0.7,
        maxCache: 8000,
        systemPrompt: "你是一个技术文档助手,用简洁清晰的中文回答。",
    });
    if (!llm) { console.error("创建失败"); return; }
    console.log(`实例 ID: ${llm.id}`);

    // ===== 2. 结构化输出(可选,跨平台自动适配) =====
    // const llm2 = AbstractLLM.OnCreat({
    //     type: "OpenAI",
    //     model: "gpt-4o",
    //     apiKey: "...",
    //     responseFormat: {
    //         type: "object",
    //         properties: {
    //             summary: { type: "string", description: "摘要" },
    //             keywords: { type: "array", items: { type: "string" } }
    //         },
    //         required: ["summary"]
    //     }
    // });

    // ===== 3. 定义工具 =====
    const tools: LLMUtility.ToolData[] = [{
        name: "search_docs",
        description: "搜索技术文档",
        inputSchema: {
            type: "object",
            properties: {
                query: { type: "string", description: "搜索关键词" },
                limit: { type: "number", description: "返回数量", minimum: 1, maximum: 10 },
            },
            required: ["query"],
        },
        handler: async (args) => {
            const results = await searchDocs(String(args.query), Number(args.limit) || 5);
            return JSON.stringify(results);
        },
    }];

    // ===== 4. 流式对话 =====
    LogUtility.LogSystem("开始对话");
    const r1 = await llm.OnSend("React useEffect 依赖数组为空是什么行为?", tools, (chunk) => {
        if (chunk.content) process.stdout.write(chunk.content);
    });
    console.log(`\n用量: ${JSON.stringify(r1.usage)}`);

    // ===== 5. 多轮对话 =====
    const r2 = await llm.OnSend("那如果传入 [count] 呢?", tools);
    console.log(r2.content);

    // ===== 6. 导出状态 =====
    const snapshot = llm.OnExport();
    console.log(`消息数: ${snapshot.length} 条`);
    console.log(`轮次: ${snapshot.filter(m => m.role === "user").length} 轮`);

    // ===== 7. 查看对话历史(从消息列表提取) =====
    logHistory(snapshot);

    // ===== 8. 清理 =====
    llm.OnDestroy();
}

function logHistory(messages: LLMUtility.MessageData[]) {
    console.log("\n=== 对话历史 ===");
    let totalUsage = { input: 0, output: 0, total: 0 };
    for (let i = 0; i < messages.length; i++) {
        const m = messages[i];
        const time = new Date(m.timestamp).toLocaleTimeString();
        if (m.role === "user") {
            console.log(`[${time}] 用户: ${m.content.slice(0, 50)}...`);
        } else if (m.role === "assistant") {
            console.log(`[${time}] 助手: ${m.content.slice(0, 80)}...`);
            // 只取每轮最后一条 assistant(下一条是 user 或末尾),避免重复计数
            const next = messages[i + 1];
            if (m.usage && (!next || next.role === "user")) {
                console.log(`        本轮用量: in=${m.usage.input} out=${m.usage.output}`);
                totalUsage.input += m.usage.input;
                totalUsage.output += m.usage.output;
                totalUsage.total += m.usage.total;
            }
        }
    }
    console.log(`累计用量: ${JSON.stringify(totalUsage)}`);
}

main().catch(console.error);