# @dm/xllm — 面向大模型 / Agent 的使用说明 本文档供 **大模型、代码助手、自动化 Agent** 在编写或修改调用代码时作为单一事实来源。API 以当前包导出为准(入口:`packages/xllm/src/index.ts`)。 --- ## 1. 包与运行时 - **包名**:`@dm/xllm` - **模块**:ESM(`"type": "module"`) - **运行环境**:需存在全局 `fetch`(现代 Node、浏览器、Bun 等)。无 `fetch` 时抛 `XllmError`(`NETWORK_ERROR`)。 - **安装(monorepo)**:在依赖包中 `bun add @dm/xllm@workspace:*` 或等价 workspace 引用。 --- ## 2. 入口与导出 ### 2.1 运行时导出 ```ts import { createXllm, XllmError } from "@dm/xllm"; ``` ### 2.2 类型导出(`export type`) `XClientOptions`, `XRequest`, `XResponse`, `XStreamEvent`, `XMessage`, `XContentPart`, `XProviderName`, `XThinkingMode`, `XReasoningEffortInput`, `XToolDefinition`, `XToolCall`, `XToolChoice`, `XToolExecutor`, `XToolExecutorMap`, `XToolErrorStrategy`, `XChatWithToolsOptions`, `XChatWithToolsResult`, `XUsage` --- ## 3. 客户端:`createXllm` 与 `XllmClient` ```ts const xllm = createXllm(options?: XClientOptions): XllmClient; ``` ### 3.1 `XllmClient` 方法 | 方法 | 签名 | 用途 | |------|------|------| | `generate` | `(request: XRequest) => Promise` | 非流式,一次返回完整结构 | | `stream` | `(request: XRequest) => AsyncIterable` | 流式 SSE,用 `for await` 消费 | | `chatWithTools` | `(request: XChatWithToolsOptions, executors: XToolExecutorMap) => Promise` | 非流式 + 自动工具闭环 | | `streamWithTools` | 同上,返回 `AsyncIterable` | 流式 + 自动工具闭环 | | `with` | `(overrides: Partial) => XllmClient` | 复制客户端并合并默认配置 | --- ## 4. 配置合并与环境变量 ### 4.1 优先级(从高到低) 1. **单次请求** `XRequest` / `XChatWithToolsOptions` 上的字段(`provider`, `model`, `apiKey`, `baseURL` 等) 2. **`createXllm` 传入的 `XClientOptions`** 3. **环境变量**(仅 Node 等存在 `process.env` 时) 实现位置:`packages/xllm/src/runtime/config.ts`。 ### 4.2 环境变量 | 变量 | 作用 | |------|------| | `XLLM_API_KEY` | 通用 API Key 兜底 | | `OPENAI_API_KEY` | `provider === "openai-compatible"` 时兜底 | | `DEEPSEEK_API_KEY` | `provider === "deepseek"` 时兜底 | | `XLLM_BASE_URL` | 通用 base URL 兜底 | ### 4.3 默认 `provider` / `model` / `baseURL` 未指定时: - `provider` 默认为 `"openai-compatible"` - `model`:`openai-compatible` → `gpt-4o-mini`,`deepseek` → `deepseek-chat` - `baseURL`:`https://api.openai.com/v1` 或 `https://api.deepseek.com` --- ## 5. 供应商(`XProviderName`) 当前内置: - `"openai-compatible"`:OpenAI Chat Completions 兼容 HTTP 形态(`/chat/completions`) - `"deepseek"`:在兼容层之上设置 DeepSeek 默认 `baseURL`,其余映射与 openai-compatible 一致 **接入其它「OpenAI 兼容」网关**:不必新增枚举;使用 `provider: "openai-compatible"`,并设置 `baseURL`(及可选 `headers`)指向该网关。 **非兼容协议**:须在库内新增 `ProviderAdapter` 并注册(见第 11 节)。 --- ## 5.1 思考模式(Thinking / CoT)— **非全供应商通用** `thinking` / `reasoningEffort` **不是**抽象意义上的「全市场通用配置」:它们对应 **OpenAI Chat Completions 兼容 JSON** 里部分厂商采用的字段名(DeepSeek 官方文档中的 OpenAI 形态即如此)。**其它供应商若使用不同字段名、不同嵌套路径或不同 API**,有两种做法: 1. **`providerExtras`**(推荐先做):在 `XRequest.providerExtras` 里写入任意键值,库会在构建完标准字段后 **`Object.assign` 合并进请求体根对象**(**后合并者可覆盖**同名标准字段)。用于传入厂商文档要求的专有参数,而无需立刻新增适配器。 2. **新 `ProviderAdapter`**:协议差异大(路径、鉴权、流式格式都不同)时,在 `packages/xllm` 内实现并注册适配器。 与 DeepSeek 文档一致的字段映射如下(兼容体): | `XRequest` 字段 | 请求体字段 | 说明 | |-----------------|------------|------| | `thinking?: { type: "enabled" \| "disabled" }` | `thinking` | 思考开关;不传则由服务端默认(一般为 enabled) | | `reasoningEffort?: "low" \| "medium" \| "high" \| "max" \| "xhigh"` | `reasoning_effort` | 仅发送 **`high`** 或 **`max`**:`low` / `medium` → `high`,`xhigh` → `max` | **流式**:若供应商在 SSE 的 `choices[].delta` 中返回 `reasoning_content`(或 `reasoning` 字符串),库会发出 `XStreamEvent`:`{ type: "reasoning.delta", text: string }`,与 `{ type: "text.delta", ... }` 区分。 **非流式**:若 `message` 中含 `reasoning_content` 或 `reasoning`,会填入 `XResponse.reasoning`;正文仍在 `XResponse.text`。 `provider: "deepseek"` 与 `provider: "openai-compatible"` 均走同一适配器请求体逻辑;其它 **同形态** 网关若支持相同字段,亦可使用 `thinking` / `reasoningEffort`;否则请用 **`providerExtras`**。 --- ## 6. 请求与消息模型 ### 6.1 `XRequest`(核心字段) - `messages: XMessage[]`(必填) - `tools?: XToolDefinition[]` - `toolChoice?: "auto" | "none" | { name: string }` - `temperature?`, `topP?`, `maxTokens?`, `metadata?` - 可覆盖:`provider?`, `model?`, `apiKey?`, `baseURL?` - `thinking?`, `reasoningEffort?`:见 §5.1(非通用,部分兼容厂商) - `providerExtras?`:合并进 Chat Completions 请求 JSON 根对象(见 §5.1) - `stream?`:由 `generate`/`stream` 内部控制,调用方一般不必依赖此字段语义 ### 6.2 `XMessage` - `system` | `user` | `assistant`:`content: XContentPart[]`;`assistant` 可选 `toolCalls?: XToolCall[]`(**协议要求**:在 `role: "tool"` 之前,assistant 需带对应 `tool_calls`;使用 `chatWithTools` / `streamWithTools` 时由库自动写入);`assistant` 还可选 **`reasoningContent?: string`**,序列化为 **`reasoning_content`**(思考模式 + 工具调用时,部分供应商要求后续轮完整回传) - `tool`:**必须**包含 `toolCallId: string`,且与上一轮 assistant 的 `tool_calls[].id` 对应 ### 6.3 `XContentPart` - `{ type: "text"; text: string }` - `{ type: "image"; image: { url: string } }`(http(s) 或 data URL;**模型是否支持多模态由供应商决定**,库只做字段映射) ### 6.4 `XToolDefinition` - `name`, 可选 `description`, `parameters`(通常为 JSON Schema 风格对象,会原样进入供应商请求体) --- ## 7. 响应与流式事件 ### 7.1 `XResponse` - `text`, `toolCalls`, `usage?`, `provider`, `model`, `finishReason?`, `raw?` ### 7.2 `XStreamEvent`(判别联合,按 `event.type` 收窄) | `type` | 含义 | |--------|------| | `response.start` | 流开始,含 `provider`, `model`, `requestId?` | | `text.delta` | 文本增量 | | `reasoning.delta` | 思考链增量(如 `reasoning_content`) | | `tool_call.delta` | 工具参数增量 | | `tool_call.done` | 单个工具调用片段结束 | | `response.usage` | 用量 | | `response.done` | 本轮流结束;`finishReason` 可能为空(例如仅 `[DONE]` 收尾时) | **消费建议**:直出场景主要处理 `text.delta`;工具场景监听 `tool_call.done`;结束可用 `response.done`(注意可能与适配器在 `finish_reason` 与 `[DONE]` 路径上的去重逻辑配合,避免重复收尾业务状态)。 --- ## 8. 工具调用 ### 8.1 仅单次模型调用(手动处理工具) 1. `generate` / `stream` 带 `tools` + `toolChoice: "auto"` 2. 从 `XResponse.toolCalls` 或流式 `tool_call.done` 取 `id`, `name`, `arguments`(`arguments` 为 JSON 字符串) 3. 本地执行后,追加 `role: "tool"` 且 `toolCallId` 对齐,再发下一轮请求;assistant 侧需带 `toolCalls`(**推荐直接用 `chatWithTools` / `streamWithTools` 避免协议错误**) ### 8.2 `chatWithTools`(非流式闭环) - 额外选项:`maxRounds?`(默认 `5`),`toolErrorStrategy?`(默认 `"throw"`) - 返回:`{ response, rounds, toolCallsExecuted }` - 行为:每轮 `generate`;若有 `toolCalls`,执行 `executors[name]`,将结果写入 tool 消息并追加历史,直到无工具调用或超出 `maxRounds` - **思考模式 + 工具调用(DeepSeek)**:若本轮 `XResponse.reasoning` 有值(来自 `message.reasoning_content`),库会把同一全文写入历史 assistant 的 `reasoningContent`,下一轮请求序列化为 `reasoning_content`,满足「后续请求须完整回传」的要求。 ### 8.3 `streamWithTools`(流式闭环) - 每轮消费完整 `stream`;收集 `tool_call.done` 与拼接的 `assistantText`;**同时拼接本轮全部 `reasoning.delta` 为 `reasoningContent`**;执行工具后追加 `assistant`(含 `toolCalls` 与可选 `reasoningContent`)与 `tool` 消息,再进入下一轮 - 事件仍原样 `yield` 给调用方 ### 8.4 `XToolExecutor` 与 `XToolExecutorMap` ```ts type XToolExecutor = (args: unknown, toolCall: XToolCall) => Promise | unknown; type XToolExecutorMap = Record; ``` - `args` 由库对 `toolCall.arguments` 做 `JSON.parse` 失败时回退为 `{ raw: string }` ### 8.5 `toolErrorStrategy` | 值 | 行为 | |----|------| | `throw` | 工具不存在或执行抛错 → 抛 `XllmError` | | `return_tool_error_message` | 将错误封装为 tool 消息内容(JSON),让模型继续 | | `skip` | 跳过该工具,不追加 tool 消息 | --- ## 9. 错误:`XllmError` - `code`: `AUTH_ERROR` | `RATE_LIMIT` | `NETWORK_ERROR` | `INVALID_REQUEST` | `PROVIDER_ERROR` - `provider`, `message`, 可选 `statusCode`, `requestId`, `raw` - HTTP 非 2xx 时由适配器 `normalizeError` 映射 --- ## 10. 自定义 `fetch` 与测试 ```ts const xllm = createXllm({ apiKey: "test", fetch: mockFetch as typeof fetch, }); ``` 用于单测或代理;`generate`/`stream` 使用合并后的 `fetchImpl`。 --- ## 11. 新增内置供应商(需改库) 1. 扩展 `XProviderName`(`packages/xllm/src/core/types.ts`) 2. 实现 `ProviderAdapter`(`packages/xllm/src/providers/types.ts`) 3. 在 `registry.ts` 注册 4. 在 `runtime/config.ts` 补充默认 `model` / `baseURL` / `resolveApiKey` 分支 5. 增加 `index.test.ts` 用 mock `fetch` 覆盖 **OpenAI 兼容第三方**:通常只需 `openai-compatible` + `baseURL`,无需新枚举。 --- ## 12. 实现文件索引(供维护者 / Agent 定位) | 职责 | 路径 | |------|------| | 对外导出 | `src/index.ts` | | 客户端 | `src/client/create-xllm.ts`, `generate.ts`, `stream.ts`, `chat-with-tools.ts`, `stream-with-tools.ts`, `tool-loop-shared.ts` | | 类型 | `src/core/types.ts`, `src/core/errors.ts` | | 配置 | `src/runtime/config.ts`, `http.ts`, `sse.ts` | | 适配器 | `src/providers/openai-compatible.adapter.ts`, `deepseek.adapter.ts`, `registry.ts` | --- ## 13. 生成代码时的检查清单(给大模型) - [ ] 是否设置了 `apiKey` 或对应环境变量? - [ ] 兼容网关是否使用 `openai-compatible` + 正确 `baseURL`? - [ ] 流式是否用 `for await` 遍历 `XStreamEvent` 并收窄 `type`? - [ ] 工具闭环是否优先使用 `chatWithTools` / `streamWithTools`,避免手写 `tool_calls` 顺序错误? - [ ] 多模态是否确认目标模型支持,否则仅发 `text`? - [ ] 工具失败策略是否为生产环境显式选择 `toolErrorStrategy`? - [ ] 思考类参数是否确认与供应商文档一致?否则是否改用 `providerExtras`? --- *文档与 `packages/xllm` 源码同步维护;若行为与代码不一致,以源码为准。*