From 8f52a10ae037bee0365f605e04b7227df66bab5b Mon Sep 17 00:00:00 2001
From: abearxiong
Date: Sun, 25 May 2025 14:01:37 +0800
Subject: [PATCH] init
---
.gitignore | 23 +++
.npmrc | 2 +
bun.config.mjs | 25 +++
package.json | 77 ++++++++
readme.md | 2 +
src/modules/logger.ts | 6 +
src/provider/chat-adapter/custom.ts | 14 ++
src/provider/chat-adapter/deepseek.ts | 10 +
src/provider/chat-adapter/model-scope.ts | 11 ++
src/provider/chat-adapter/ollama.ts | 47 +++++
src/provider/chat-adapter/siliconflow.ts | 39 ++++
src/provider/chat-adapter/volces.ts | 10 +
src/provider/core/chat.ts | 143 ++++++++++++++
src/provider/core/index.ts | 27 +++
src/provider/core/text-regex.ts | 105 ++++++++++
src/provider/core/type.ts | 29 +++
src/provider/index.ts | 63 ++++++
src/provider/knowledge/knowledge-base.ts | 107 +++++++++++
src/provider/knowledge/knowledge.ts | 7 +
src/provider/media/index.ts | 1 +
src/provider/media/video/siliconflow.ts | 37 ++++
src/provider/utils/ai-config-type.ts | 52 +++++
src/provider/utils/chunk.ts | 86 +++++++++
src/provider/utils/parse-config.ts | 206 ++++++++++++++++++++
src/provider/utils/token.ts | 34 ++++
src/test/chunks/01-get.ts | 65 +++++++
src/test/encrypt/index.ts | 9 +
src/test/func-call/curl.sh | 35 ++++
src/test/func-call/demo.ts | 116 ++++++++++++
src/test/model-scope/index.ts | 26 +++
src/test/ollama-knowledge.ts | 37 ++++
src/test/ollama.ts | 86 +++++++++
src/test/provider/index.ts | 7 +
src/test/siliconflow/common.ts | 13 ++
src/test/siliconflow/get.ts | 22 +++
src/test/siliconflow/knowledge/create.ts | 18 ++
src/test/siliconflow/knowledge/qwen.md | 232 +++++++++++++++++++++++
src/test/siliconflow/videos/index.ts | 100 ++++++++++
tsconfig.json | 16 ++
videos/my_speech_text.mp3 | Bin 0 -> 20420 bytes
videos/my_speech_text.txt | 1 +
videos/my_speech_text.wav | Bin 0 -> 212992 bytes
42 files changed, 1946 insertions(+)
create mode 100644 .gitignore
create mode 100644 .npmrc
create mode 100644 bun.config.mjs
create mode 100644 package.json
create mode 100644 readme.md
create mode 100644 src/modules/logger.ts
create mode 100644 src/provider/chat-adapter/custom.ts
create mode 100644 src/provider/chat-adapter/deepseek.ts
create mode 100644 src/provider/chat-adapter/model-scope.ts
create mode 100644 src/provider/chat-adapter/ollama.ts
create mode 100644 src/provider/chat-adapter/siliconflow.ts
create mode 100644 src/provider/chat-adapter/volces.ts
create mode 100644 src/provider/core/chat.ts
create mode 100644 src/provider/core/index.ts
create mode 100644 src/provider/core/text-regex.ts
create mode 100644 src/provider/core/type.ts
create mode 100644 src/provider/index.ts
create mode 100644 src/provider/knowledge/knowledge-base.ts
create mode 100644 src/provider/knowledge/knowledge.ts
create mode 100644 src/provider/media/index.ts
create mode 100644 src/provider/media/video/siliconflow.ts
create mode 100644 src/provider/utils/ai-config-type.ts
create mode 100644 src/provider/utils/chunk.ts
create mode 100644 src/provider/utils/parse-config.ts
create mode 100644 src/provider/utils/token.ts
create mode 100644 src/test/chunks/01-get.ts
create mode 100644 src/test/encrypt/index.ts
create mode 100644 src/test/func-call/curl.sh
create mode 100644 src/test/func-call/demo.ts
create mode 100644 src/test/model-scope/index.ts
create mode 100644 src/test/ollama-knowledge.ts
create mode 100644 src/test/ollama.ts
create mode 100644 src/test/provider/index.ts
create mode 100644 src/test/siliconflow/common.ts
create mode 100644 src/test/siliconflow/get.ts
create mode 100644 src/test/siliconflow/knowledge/create.ts
create mode 100644 src/test/siliconflow/knowledge/qwen.md
create mode 100644 src/test/siliconflow/videos/index.ts
create mode 100644 tsconfig.json
create mode 100644 videos/my_speech_text.mp3
create mode 100644 videos/my_speech_text.txt
create mode 100644 videos/my_speech_text.wav
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbe3665
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+node_modules
+
+dist
+
+app.config.json5
+
+apps.config.json
+
+deploy.tar.gz
+cache-file
+
+/apps
+
+logs
+
+.env*
+!.env.example
+
+config.json
+
+pack-dist
+
+videos/output*
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..7446745
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
+//registry.npmjs.org/:_authToken=${NPM_TOKEN}
\ No newline at end of file
diff --git a/bun.config.mjs b/bun.config.mjs
new file mode 100644
index 0000000..7b1d84a
--- /dev/null
+++ b/bun.config.mjs
@@ -0,0 +1,25 @@
+// @ts-check
+// https://bun.sh/docs/bundler
+// @ts-ignore
+import { resolvePath } from '@kevisual/use-config/env';
+import pkg from './package.json';
+import { execSync } from 'node:child_process';
+
+// bun run src/index.ts --
+await Bun.build({
+ target: 'node',
+ format: 'esm',
+ entrypoints: [resolvePath('./src/provider/index.ts')],
+ outdir: resolvePath('./dist'),
+ naming: {
+ entry: 'ai-provider.js',
+ },
+
+ define: {
+ ENVISION_VERSION: JSON.stringify(pkg.version),
+ },
+ env: 'ENVISION_*',
+});
+
+const cmd = 'dts -i src/provider/index.ts -o ai-provider.d.ts';
+execSync(cmd, { stdio: 'inherit' });
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..806c5aa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@kevisual/ai",
+ "version": "0.0.4",
+ "description": "后面需要把ai-center的provider模块提取出去",
+ "main": "index.js",
+ "basename": "/root/ai-center-services",
+ "app": {
+ "entry": "dist/app.mjs",
+ "key": "ai-center-services",
+ "type": "system-app"
+ },
+ "files": [
+ "dist",
+ "types"
+ ],
+ "scripts": {
+ "build": "npm run clean && bun bun.config.mjs",
+ "dev": "bun run --watch bun.config.mjs",
+ "test": "tsx test/**/*.ts",
+ "clean": "rm -rf dist",
+ "pub": "envision pack -p -u"
+ },
+ "keywords": [],
+ "author": "abearxiong (https://www.xiongxiao.me)",
+ "license": "MIT",
+ "packageManager": "pnpm@10.11.0",
+ "type": "module",
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org/",
+ "access": "public"
+ },
+ "exports": {
+ ".": {
+ "import": "./dist/ai-provider.js",
+ "types": "./dist/ai-provider.d.ts"
+ },
+ "./ai-provider": {
+ "import": "./dist/ai-provider.js",
+ "types": "./dist/ai-provider.d.ts"
+ }
+ },
+ "devDependencies": {
+ "@kevisual/code-center-module": "0.0.19",
+ "@kevisual/mark": "0.0.7",
+ "@kevisual/router": "0.0.21",
+ "@kevisual/types": "^0.0.10",
+ "@kevisual/use-config": "^1.0.17",
+ "@types/bun": "^1.2.14",
+ "@types/crypto-js": "^4.2.2",
+ "@types/formidable": "^3.4.5",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^22.15.21",
+ "@vitejs/plugin-basic-ssl": "^2.0.0",
+ "cookie": "^1.0.2",
+ "cross-env": "^7.0.3",
+ "crypto-js": "^4.2.0",
+ "dayjs": "^1.11.13",
+ "dotenv": "^16.5.0",
+ "formidable": "^3.5.4",
+ "ioredis": "^5.6.1",
+ "json5": "^2.2.3",
+ "lodash-es": "^4.17.21",
+ "openai": "4.103.0",
+ "pm2": "^6.0.6",
+ "rimraf": "^6.0.1",
+ "rollup": "^4.41.0",
+ "rollup-plugin-dts": "^6.2.1",
+ "sequelize": "^6.37.7",
+ "tape": "^5.9.0",
+ "tiktoken": "^1.0.21",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5"
+ },
+ "dependencies": {
+ "@kevisual/logger": "^0.0.4"
+ }
+}
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..b6c8126
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,2 @@
+# AI Center
+
diff --git a/src/modules/logger.ts b/src/modules/logger.ts
new file mode 100644
index 0000000..c54a2ab
--- /dev/null
+++ b/src/modules/logger.ts
@@ -0,0 +1,6 @@
+import { Logger } from '@kevisual/logger';
+
+export const logger = new Logger({
+ level: process?.env?.LOG_LEVEL || 'info',
+ showTime: true,
+});
diff --git a/src/provider/chat-adapter/custom.ts b/src/provider/chat-adapter/custom.ts
new file mode 100644
index 0000000..01af801
--- /dev/null
+++ b/src/provider/chat-adapter/custom.ts
@@ -0,0 +1,14 @@
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+
+export type OllamaOptions = BaseChatOptions;
+
+/**
+ * 自定义模型
+ */
+export class Custom extends BaseChat {
+ static BASE_URL = 'https://api.deepseek.com/v1/';
+ constructor(options: OllamaOptions) {
+ const baseURL = options.baseURL || Custom.BASE_URL;
+ super({ ...(options as BaseChatOptions), baseURL: baseURL });
+ }
+}
diff --git a/src/provider/chat-adapter/deepseek.ts b/src/provider/chat-adapter/deepseek.ts
new file mode 100644
index 0000000..b2bdfc1
--- /dev/null
+++ b/src/provider/chat-adapter/deepseek.ts
@@ -0,0 +1,10 @@
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+
+export type DeepSeekOptions = Partial;
+export class DeepSeek extends BaseChat {
+ static BASE_URL = 'https://api.deepseek.com/v1/';
+ constructor(options: DeepSeekOptions) {
+ const baseURL = options.baseURL || DeepSeek.BASE_URL;
+ super({ ...(options as BaseChatOptions), baseURL: baseURL });
+ }
+}
diff --git a/src/provider/chat-adapter/model-scope.ts b/src/provider/chat-adapter/model-scope.ts
new file mode 100644
index 0000000..6212b72
--- /dev/null
+++ b/src/provider/chat-adapter/model-scope.ts
@@ -0,0 +1,11 @@
+// https://api-inference.modelscope.cn/v1/
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+
+export type ModelScopeOptions = Partial;
+export class ModelScope extends BaseChat {
+ static BASE_URL = 'https://api-inference.modelscope.cn/v1/';
+ constructor(options: ModelScopeOptions) {
+ const baseURL = options.baseURL || ModelScope.BASE_URL;
+ super({ ...options, baseURL: baseURL } as any);
+ }
+}
diff --git a/src/provider/chat-adapter/ollama.ts b/src/provider/chat-adapter/ollama.ts
new file mode 100644
index 0000000..994e35e
--- /dev/null
+++ b/src/provider/chat-adapter/ollama.ts
@@ -0,0 +1,47 @@
+import { BaseChat, BaseChatOptions } from '../core/index.ts';
+import type { ChatMessage, ChatMessageOptions } from '../core/index.ts';
+
+export type OllamaOptions = Partial;
+
+type OllamaModel = {
+ name: string;
+ model: string;
+ modified_at: string;
+
+ size: number;
+ digest: string;
+ details: {
+ parent_model: string;
+ format: string; // example: gguf
+ family: string; // example qwen
+ families: string[];
+ parameter_size: string;
+ quantization_level: string; // example: Q4_K_M Q4_0
+ };
+};
+export class Ollama extends BaseChat {
+ static BASE_URL = 'http://localhost:11434/v1';
+ constructor(options: OllamaOptions) {
+ const baseURL = options.baseURL || Ollama.BASE_URL;
+ super({ ...(options as BaseChatOptions), baseURL: baseURL });
+ }
+ async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
+ const res = await super.chat(messages, options);
+ console.log('thunk', this.getChatUsage());
+ return res;
+ }
+ /**
+ * 获取模型列表
+ * @returns
+ */
+ async listModels(): Promise<{ models: OllamaModel[] }> {
+ const _url = new URL(this.baseURL);
+ const tagsURL = new URL('/api/tags', _url);
+ return this.openai.get(tagsURL.toString());
+ }
+ async listRunModels(): Promise<{ models: OllamaModel[] }> {
+ const _url = new URL(this.baseURL);
+ const tagsURL = new URL('/api/ps', _url);
+ return this.openai.get(tagsURL.toString());
+ }
+}
diff --git a/src/provider/chat-adapter/siliconflow.ts b/src/provider/chat-adapter/siliconflow.ts
new file mode 100644
index 0000000..77b91ad
--- /dev/null
+++ b/src/provider/chat-adapter/siliconflow.ts
@@ -0,0 +1,39 @@
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+import { OpenAI } from 'openai';
+
+export type SiliconFlowOptions = Partial;
+
+type SiliconFlowUsageData = {
+ id: string;
+ name: string;
+ image: string;
+ email: string;
+ isAdmin: boolean;
+ balance: string;
+ status: 'normal' | 'suspended' | 'expired' | string; // 状态
+ introduce: string;
+ role: string;
+ chargeBalance: string;
+ totalBalance: string;
+ category: string;
+};
+type SiliconFlowUsageResponse = {
+ code: number;
+ message: string;
+ status: boolean;
+ data: SiliconFlowUsageData;
+};
+export class SiliconFlow extends BaseChat {
+ static BASE_URL = 'https://api.siliconflow.cn/v1';
+ constructor(options: SiliconFlowOptions) {
+ const baseURL = options.baseURL || SiliconFlow.BASE_URL;
+ super({ ...(options as BaseChatOptions), baseURL: baseURL });
+ }
+ async getUsageInfo(): Promise {
+ return this.openai.get('/user/info');
+ }
+ async chat(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], options?: Partial) {
+ const res = await super.chat(messages, options);
+ return res;
+ }
+}
diff --git a/src/provider/chat-adapter/volces.ts b/src/provider/chat-adapter/volces.ts
new file mode 100644
index 0000000..7144ad4
--- /dev/null
+++ b/src/provider/chat-adapter/volces.ts
@@ -0,0 +1,10 @@
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+
+export type VolcesOptions = Partial;
+export class Volces extends BaseChat {
+ static BASE_URL = 'https://ark.cn-beijing.volces.com/api/v3/';
+ constructor(options: VolcesOptions) {
+ const baseURL = options.baseURL || Volces.BASE_URL;
+ super({ ...(options as BaseChatOptions), baseURL: baseURL });
+ }
+}
diff --git a/src/provider/core/chat.ts b/src/provider/core/chat.ts
new file mode 100644
index 0000000..38a3f5f
--- /dev/null
+++ b/src/provider/core/chat.ts
@@ -0,0 +1,143 @@
+import { OpenAI } from 'openai';
+import type {
+ BaseChatInterface,
+ ChatMessageComplete,
+ ChatMessage,
+ ChatMessageOptions,
+ BaseChatUsageInterface,
+ ChatStream,
+ EmbeddingMessage,
+ EmbeddingMessageComplete,
+} from './type.ts';
+
+export type BaseChatOptions> = {
+ /**
+ * 默认baseURL
+ */
+ baseURL: string;
+ /**
+ * 默认模型
+ */
+ model?: string;
+ /**
+ * 默认apiKey
+ */
+ apiKey: string;
+ /**
+ * 是否在浏览器中使用
+ */
+ isBrowser?: boolean;
+ /**
+ * 是否流式输出, 默认 false
+ */
+ stream?: boolean;
+} & T;
+
+export class BaseChat implements BaseChatInterface, BaseChatUsageInterface {
+ /**
+ * 默认baseURL
+ */
+ baseURL: string;
+ /**
+ * 默认模型
+ */
+ model: string;
+ /**
+ * 默认apiKey
+ */
+ apiKey: string;
+ /**
+ * 是否在浏览器中使用
+ */
+ isBrowser: boolean;
+ /**
+ * openai实例
+ */
+ openai: OpenAI;
+
+ prompt_tokens: number;
+ total_tokens: number;
+ completion_tokens: number;
+
+ constructor(options: BaseChatOptions) {
+ this.baseURL = options.baseURL;
+ this.model = options.model;
+ this.apiKey = options.apiKey;
+ this.isBrowser = options.isBrowser ?? false;
+ this.openai = new OpenAI({
+ apiKey: this.apiKey,
+ baseURL: this.baseURL,
+ dangerouslyAllowBrowser: this.isBrowser,
+ });
+ }
+ /**
+ * 聊天
+ */
+ async chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise {
+ const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
+ model: this.model,
+ messages,
+ ...options,
+ stream: false,
+ };
+ const res = (await this.openai.chat.completions.create(createParams)) as ChatMessageComplete;
+ this.prompt_tokens = res.usage?.prompt_tokens ?? 0;
+ this.total_tokens = res.usage?.total_tokens ?? 0;
+ this.completion_tokens = res.usage?.completion_tokens ?? 0;
+ return res;
+ }
+ async chatStream(messages: ChatMessage[], options?: ChatMessageOptions) {
+ const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
+ model: this.model,
+ messages,
+ ...options,
+ stream: true,
+ };
+ if (createParams.response_format) {
+ throw new Error('response_format is not supported in stream mode');
+ }
+ return this.openai.chat.completions.create(createParams) as unknown as ChatStream;
+ }
+
+ /**
+ * 测试
+ */
+ test() {
+ return this.chat([{ role: 'user', content: 'Hello, world!' }]);
+ }
+ /**
+ * 获取聊天使用情况
+ * @returns
+ */
+ getChatUsage() {
+ return {
+ prompt_tokens: this.prompt_tokens,
+ total_tokens: this.total_tokens,
+ completion_tokens: this.completion_tokens,
+ };
+ }
+ getHeaders(headers?: Record) {
+ return {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.apiKey}`,
+ ...headers,
+ };
+ }
+ /**
+ * 生成embedding 内部
+ * @param text
+ * @returns
+ */
+ async generateEmbeddingCore(text: string | string[], options?: EmbeddingMessage): Promise {
+ const embeddingModel = options?.model || this.model;
+ const res = await this.openai.embeddings.create({
+ model: embeddingModel,
+ input: text,
+ encoding_format: 'float',
+ ...options,
+ });
+ this.prompt_tokens += res.usage.prompt_tokens;
+ this.total_tokens += res.usage.total_tokens;
+ return res;
+ }
+}
diff --git a/src/provider/core/index.ts b/src/provider/core/index.ts
new file mode 100644
index 0000000..732dc35
--- /dev/null
+++ b/src/provider/core/index.ts
@@ -0,0 +1,27 @@
+import { ChatStream } from './type.ts';
+
+// export type { BaseChat, BaseChatOptions } from './chat.ts';
+export * from './chat.ts'
+// export {
+// ChatMessage,
+// ChatMessageOptions, //
+// ChatMessageComplete,
+// ChatMessageStream,
+// BaseChatInterface,
+// BaseChatUsageInterface,
+// ChatStream,
+// EmbeddingMessage,
+// EmbeddingMessageComplete,
+// } from './type.ts';
+export * from './type.ts'
+/**
+ * for await (const chunk of chatStream) {
+ * console.log(chunk);
+ * }
+ * @param chatStream
+ */
+export const readStream = async (chatStream: ChatStream) => {
+ for await (const chunk of chatStream) {
+ console.log(chunk);
+ }
+};
diff --git a/src/provider/core/text-regex.ts b/src/provider/core/text-regex.ts
new file mode 100644
index 0000000..834e420
--- /dev/null
+++ b/src/provider/core/text-regex.ts
@@ -0,0 +1,105 @@
+// Updated: Aug. 20, 2024
+// Live demo: https://jina.ai/tokenizer
+// LICENSE: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0)
+// COPYRIGHT: Jina AI
+
+// Define variables for magic numbers
+const MAX_HEADING_LENGTH = 7;
+const MAX_HEADING_CONTENT_LENGTH = 200;
+const MAX_HEADING_UNDERLINE_LENGTH = 200;
+const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100;
+const MAX_LIST_ITEM_LENGTH = 200;
+const MAX_NESTED_LIST_ITEMS = 6;
+const MAX_LIST_INDENT_SPACES = 7;
+const MAX_BLOCKQUOTE_LINE_LENGTH = 200;
+const MAX_BLOCKQUOTE_LINES = 15;
+const MAX_CODE_BLOCK_LENGTH = 1500;
+const MAX_CODE_LANGUAGE_LENGTH = 20;
+const MAX_INDENTED_CODE_LINES = 20;
+const MAX_TABLE_CELL_LENGTH = 200;
+const MAX_TABLE_ROWS = 20;
+const MAX_HTML_TABLE_LENGTH = 2000;
+const MIN_HORIZONTAL_RULE_LENGTH = 3;
+const MAX_SENTENCE_LENGTH = 400;
+const MAX_QUOTED_TEXT_LENGTH = 300;
+const MAX_PARENTHETICAL_CONTENT_LENGTH = 200;
+const MAX_NESTED_PARENTHESES = 5;
+const MAX_MATH_INLINE_LENGTH = 100;
+const MAX_MATH_BLOCK_LENGTH = 500;
+const MAX_PARAGRAPH_LENGTH = 1000;
+const MAX_STANDALONE_LINE_LENGTH = 800;
+const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100;
+const MAX_HTML_TAG_CONTENT_LENGTH = 1000;
+const LOOKAHEAD_RANGE = 100; // Number of characters to look ahead for a sentence boundary
+
+const AVOID_AT_START = `[\\s\\]})>,']`;
+const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`;
+const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`;
+const SENTENCE_END = `(?:${PUNCTUATION}(?]{0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:)?(?:\\r?\\n|$))` +
+ "|" +
+ // New pattern for citations
+ `(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` +
+ "|" +
+ // 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints)
+ `(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}` +
+ `(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` +
+ `(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` +
+ "|" +
+ // 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints)
+ `(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,${MAX_BLOCKQUOTE_LINES}})` +
+ "|" +
+ // 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints)
+ `(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` +
+ `|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,${MAX_INDENTED_CODE_LINES}}\\r?\\n?)` +
+ `|(?:(?:)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:
)?
))` +
+ "|" +
+ // 5. Tables (Markdown, grid tables, and HTML tables, with length constraints)
+ `(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` +
+ `|[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?
))` +
+ "|" +
+ // 6. Horizontal rules (Markdown and HTML hr tag)
+ `(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|
)` +
+ "|" +
+ // 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints)
+ `(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}(?:[a-zA-Z]+>)?(?:\\r?\\n|$))` +
+ "|" +
+ // 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation)
+ `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` +
+ "|" +
+ // 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints)
+ "(?:" +
+ `(?)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:
)?(?=\\r?\\n\\r?\\n|$))` +
+ "|" +
+ // 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints)
+ `(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?[a-zA-Z]+>|\\s*/>))` +
+ "|" +
+ // 12. LaTeX-style math expressions (inline and block, with length constraints)
+ `(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` +
+ "|" +
+ // 14. Fallback for any remaining content (with length constraints)
+ `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` +
+ ")",
+ "gmu"
+);
+
diff --git a/src/provider/core/type.ts b/src/provider/core/type.ts
new file mode 100644
index 0000000..f4136a1
--- /dev/null
+++ b/src/provider/core/type.ts
@@ -0,0 +1,29 @@
+import OpenAI from 'openai';
+
+export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
+export type ChatMessageOptions = Partial;
+export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion;
+export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion;
+
+export type EmbeddingMessage = Partial;
+export type EmbeddingMessageComplete = OpenAI.Embeddings.CreateEmbeddingResponse;
+export interface BaseChatInterface {
+ chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise;
+}
+
+export interface BaseChatUsageInterface {
+ /**
+ * 提示词令牌
+ */
+ prompt_tokens: number;
+ /**
+ * 总令牌
+ */
+ total_tokens: number;
+ /**
+ * 完成令牌
+ */
+ completion_tokens: number;
+}
+
+export type ChatStream = AsyncGenerator;
diff --git a/src/provider/index.ts b/src/provider/index.ts
new file mode 100644
index 0000000..5d265c5
--- /dev/null
+++ b/src/provider/index.ts
@@ -0,0 +1,63 @@
+export * from './core/index.ts';
+import { BaseChat } from './core/chat.ts';
+
+import { Ollama } from './chat-adapter/ollama.ts';
+import { SiliconFlow } from './chat-adapter/siliconflow.ts';
+import { Custom } from './chat-adapter/custom.ts';
+import { Volces } from './chat-adapter/volces.ts';
+import { DeepSeek } from './chat-adapter/deepseek.ts';
+import { ModelScope } from './chat-adapter/model-scope.ts';
+import { ChatMessage } from './core/type.ts';
+
+export const OllamaProvider = Ollama;
+export const SiliconFlowProvider = SiliconFlow;
+export const CustomProvider = Custom;
+export const VolcesProvider = Volces;
+export const DeepSeekProvider = DeepSeek;
+export const ModelScopeProvider = ModelScope;
+
+export const ProviderMap = {
+ Ollama: OllamaProvider,
+ SiliconFlow: SiliconFlowProvider,
+ Custom: CustomProvider,
+ Volces: VolcesProvider,
+ DeepSeek: DeepSeekProvider,
+ ModelScope: ModelScopeProvider,
+ BaseChat: BaseChat,
+};
+
+type ProviderManagerConfig = {
+ provider: string;
+ model: string;
+ apiKey: string;
+ baseURL?: string;
+};
+export class ProviderManager {
+ provider: BaseChat;
+ constructor(config: ProviderManagerConfig) {
+ const { provider, model, apiKey, baseURL } = config;
+ const Provider = ProviderMap[provider] as typeof BaseChat;
+ if (!Provider) {
+ throw new Error(`Provider ${provider} not found`);
+ }
+ const providerConfig = {
+ model,
+ apiKey,
+ baseURL,
+ };
+ if (!providerConfig.baseURL) {
+ delete providerConfig.baseURL;
+ }
+ this.provider = new Provider(providerConfig);
+ }
+ static async createProvider(config: ProviderManagerConfig) {
+ if (!config.baseURL) {
+ delete config.baseURL;
+ }
+ const pm = new ProviderManager(config);
+ return pm.provider;
+ }
+ async chat(messages: ChatMessage[]) {
+ return this.provider.chat(messages);
+ }
+}
diff --git a/src/provider/knowledge/knowledge-base.ts b/src/provider/knowledge/knowledge-base.ts
new file mode 100644
index 0000000..e12829d
--- /dev/null
+++ b/src/provider/knowledge/knowledge-base.ts
@@ -0,0 +1,107 @@
+import { BaseChat, BaseChatOptions } from '../core/chat.ts';
+import { EmbeddingMessage } from '../core/type.ts';
+
+export type KnowledgeOptions> = BaseChatOptions<
+ {
+ embeddingModel: string;
+ splitSize?: number; // 分块大小 默认 2000
+ splitOverlap?: number; // 分块重叠 默认 200
+ batchSize?: number; // 批量大小 默认 4, 4*2000=8000
+ } & T
+>;
+/**
+ * 知识库构建
+ * 1. Embedding generate
+ * 2. retriever
+ * 3. reranker
+ */
+export class KnowledgeBase extends BaseChat {
+ embeddingModel: string;
+ splitSize: number;
+ splitOverlap: number;
+ batchSize: number;
+ constructor(options: KnowledgeOptions) {
+ super(options);
+ this.embeddingModel = options.embeddingModel;
+ this.splitSize = options.splitSize || 2000;
+ this.splitOverlap = options.splitOverlap || 200;
+ this.prompt_tokens = 0;
+ this.total_tokens = 0;
+ this.batchSize = options.batchSize || 4;
+ }
+ /**
+ * 生成embedding
+ * @param text
+ * @returns
+ */
+ async generateEmbedding(text: string | string[]) {
+ try {
+ const res = await this.generateEmbeddingCore(text, { model: this.embeddingModel });
+ return { code: 200, data: res.data };
+ } catch (error) {
+ const has413 = error?.message?.includes('413');
+ if (has413) {
+ return {
+ code: 413,
+ message: '请求过大,请分割文本',
+ };
+ }
+ return {
+ code: error?.code || 500,
+ message: '生成embedding失败',
+ };
+ }
+ }
+ /**
+ * 批量生成embedding
+ * @param text
+ * @returns
+ */
+ async generateEmbeddingBatch(textArray: string[]) {
+ const batchSize = this.batchSize || 4;
+ const embeddings: number[][] = [];
+ for (let i = 0; i < textArray.length; i += batchSize) {
+ const batch = textArray.slice(i, i + batchSize);
+ const res = await this.generateEmbedding(batch);
+ if (res.code === 200) {
+ embeddings.push(...res.data.map((item) => item.embedding));
+ }
+ }
+ return embeddings;
+ }
+ /**
+ * 分割长文本, 生成对应的embedding
+ * @param text
+ * @returns
+ */
+ async splitLongText(text: string) {
+ // 分割文本
+ const chunks: string[] = [];
+ let startIndex = 0;
+
+ while (startIndex < text.length) {
+ // 计算当前chunk的结束位置
+ const endIndex = Math.min(startIndex + this.splitSize, text.length);
+
+ // 提取当前chunk
+ const chunk = text.substring(startIndex, endIndex);
+ chunks.push(chunk);
+
+ // 移动到下一个起始位置,考虑重叠
+ startIndex = endIndex - this.splitOverlap;
+
+ // 如果下一个起始位置已经超出或者太接近文本结尾,就结束循环
+ if (startIndex >= text.length - this.splitOverlap) {
+ break;
+ }
+ }
+
+ // 为每个chunk生成embedding
+ const embeddings = await this.generateEmbeddingBatch(chunks);
+ // 返回文本片段和对应的embedding
+ return chunks.map((chunk, index) => ({
+ text: chunk,
+ embedding: embeddings[index],
+ }));
+ }
+}
diff --git a/src/provider/knowledge/knowledge.ts b/src/provider/knowledge/knowledge.ts
new file mode 100644
index 0000000..1337510
--- /dev/null
+++ b/src/provider/knowledge/knowledge.ts
@@ -0,0 +1,7 @@
+import { KnowledgeBase, KnowledgeOptions } from './knowledge-base.ts';
+
+export class Knowledge extends KnowledgeBase {
+ constructor(options: KnowledgeOptions) {
+ super(options);
+ }
+}
diff --git a/src/provider/media/index.ts b/src/provider/media/index.ts
new file mode 100644
index 0000000..82f6ce0
--- /dev/null
+++ b/src/provider/media/index.ts
@@ -0,0 +1 @@
+export * from './video/siliconflow.ts';
diff --git a/src/provider/media/video/siliconflow.ts b/src/provider/media/video/siliconflow.ts
new file mode 100644
index 0000000..e6040ba
--- /dev/null
+++ b/src/provider/media/video/siliconflow.ts
@@ -0,0 +1,37 @@
+import { SiliconFlow } from '../../chat-adapter/siliconflow.ts';
+export class VideoSiliconFlow extends SiliconFlow {
+ constructor(opts: any) {
+ super(opts);
+ }
+
+ async uploadAudioVoice(audioBase64: string | Blob | File) {
+ const pathname = 'uploads/audio/voice';
+ const url = `${this.baseURL}/${pathname}`;
+ const headers = {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: `Bearer ${this.apiKey}`,
+ };
+ const formData = new FormData();
+ // formData.append('audio', 'data:audio/mpeg;base64,aGVsbG93b3JsZA==');
+ // formData.append('audio', audioBase64);
+ formData.append('file', audioBase64);
+ formData.append('model', 'FunAudioLLM/CosyVoice2-0.5B');
+ formData.append('customName', 'test_name');
+ formData.append('text', '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始');
+
+ const res = await fetch(url, {
+ method: 'POST',
+ headers,
+ body: formData,
+ }).then((res) => res.json());
+ console.log('uploadAudioVoice', res);
+ }
+ async audioSpeech() {
+ this.openai.audio.speech.create({
+ model: 'FunAudioLLM/CosyVoice2-0.5B',
+ voice: 'alloy',
+ input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始',
+ response_format: 'mp3',
+ });
+ }
+}
diff --git a/src/provider/utils/ai-config-type.ts b/src/provider/utils/ai-config-type.ts
new file mode 100644
index 0000000..a4ed0c6
--- /dev/null
+++ b/src/provider/utils/ai-config-type.ts
@@ -0,0 +1,52 @@
+import type { Permission } from '@kevisual/permission';
+
+export type AIModel = {
+ /**
+ * 提供商
+ */
+ provider: string;
+ /**
+ * 模型名称
+ */
+ model: string;
+ /**
+ * 模型组
+ */
+ group: string;
+ /**
+ * 每日请求频率限制
+ */
+ dayLimit?: number;
+ /**
+ * 总的token限制
+ */
+ tokenLimit?: number;
+};
+export type SecretKey = {
+ /**
+ * 组
+ */
+ group: string;
+ /**
+ * API密钥
+ */
+ apiKey: string;
+ /**
+ * 解密密钥
+ */
+ decryptKey?: string;
+};
+
+export type AIConfig = {
+ title?: string;
+ description?: string;
+ models: AIModel[];
+ secretKeys: SecretKey[];
+ permission?: Permission;
+ filter?: {
+ objectKey: string;
+ type: 'array' | 'object';
+ operate: 'removeAttribute' | 'remove';
+ attribute: string[];
+ }[];
+};
diff --git a/src/provider/utils/chunk.ts b/src/provider/utils/chunk.ts
new file mode 100644
index 0000000..f6d3f88
--- /dev/null
+++ b/src/provider/utils/chunk.ts
@@ -0,0 +1,86 @@
+import { numTokensFromString } from './token.ts';
+
+// 常量定义
+const CHUNK_SIZE = 512; // 每个chunk的最大token数
+const MAGIC_SEPARATOR = '🦛';
+const DELIMITER = [',', '.', '!', '?', '\n', ',', '。', '!', '?'];
+const PARAGRAPH_DELIMITER = '\n\n';
+
+export interface Chunk {
+ chunkId: number;
+ text: string;
+ tokens: number;
+}
+
+/**
+ * 确保每个chunk的大小不超过最大token数
+ * @param chunk 输入的文本块
+ * @returns 分割后的文本块及其token数的数组
+ */
+function ensureChunkSize(chunk: string): Array<[string, number]> {
+ const tokens = numTokensFromString(chunk);
+ if (tokens <= CHUNK_SIZE) {
+ return [[chunk, tokens]];
+ }
+
+ // 在分隔符后添加魔法分隔符
+ let processedChunk = chunk;
+ for (const delimiter of DELIMITER) {
+ // 转义特殊字符
+ const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ processedChunk = processedChunk.replace(new RegExp(escapedDelimiter, 'g'), delimiter + MAGIC_SEPARATOR);
+ }
+
+ const chunks: Array<[string, number]> = [];
+ let tail = '';
+
+ // 按CHUNK_SIZE分割文本
+ for (let i = 0; i < processedChunk.length; i += CHUNK_SIZE) {
+ const sentences = (processedChunk.slice(i, i + CHUNK_SIZE) + ' ').split(MAGIC_SEPARATOR);
+ const currentChunk = tail + sentences.slice(0, -1).join('');
+ if (currentChunk.trim()) {
+ const tokenCount = numTokensFromString(currentChunk);
+ chunks.push([currentChunk, tokenCount]);
+ }
+ tail = sentences[sentences.length - 1].trim();
+ }
+
+ // 处理最后剩余的tail
+ if (tail) {
+ const tokenCount = numTokensFromString(tail);
+ chunks.push([tail, tokenCount]);
+ }
+
+ return chunks;
+}
+
+/**
+ * 将文本分割成chunks
+ * @param text 输入文本
+ * @returns 分割后的chunks数组
+ */
+export async function getChunks(text: string): Promise {
+ // 按段落分割文本
+ const paragraphs = text
+ .split(PARAGRAPH_DELIMITER)
+ .map((p) => p.trim())
+ .filter((p) => p);
+
+ const chunks: Chunk[] = [];
+ let currentIndex = 0;
+
+ // 处理每个段落
+ for (const paragraph of paragraphs) {
+ const splittedParagraph = ensureChunkSize(paragraph);
+ for (const [text, tokens] of splittedParagraph) {
+ chunks.push({
+ chunkId: currentIndex,
+ text,
+ tokens,
+ });
+ currentIndex++;
+ }
+ }
+
+ return chunks;
+}
diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts
new file mode 100644
index 0000000..b8d02a5
--- /dev/null
+++ b/src/provider/utils/parse-config.ts
@@ -0,0 +1,206 @@
+import { Permission } from '@kevisual/permission';
+import AES from 'crypto-js/aes.js';
+import Utf8 from 'crypto-js/enc-utf8.js';
+
+const CryptoJS = { AES, enc: { Utf8 } };
+// 加密函数
+export function encryptAES(plainText: string, secretKey: string) {
+ return CryptoJS.AES.encrypt(plainText, secretKey).toString();
+}
+
+// 解密函数
+export function decryptAES(cipherText: string, secretKey: string) {
+ const bytes = CryptoJS.AES.decrypt(cipherText, secretKey);
+ return bytes.toString(CryptoJS.enc.Utf8);
+}
+
+type AIModel = {
+ /**
+ * 提供商
+ */
+ provider: string;
+ /**
+ * 模型名称
+ */
+ model: string;
+ /**
+ * 模型组
+ */
+ group: string;
+ /**
+ * 每日请求频率限制
+ */
+ dayLimit?: number;
+ /**
+ * 总的token限制
+ */
+ tokenLimit?: number;
+};
+
+type SecretKey = {
+ /**
+ * 组
+ */
+ group: string;
+ /**
+ * API密钥
+ */
+ apiKey: string;
+ /**
+ * 解密密钥
+ */
+ decryptKey?: string;
+};
+
+export type GetProviderOpts = {
+ model: string;
+ group: string;
+ decryptKey?: string;
+};
+export type ProviderResult = {
+ provider: string;
+ model: string;
+ group: string;
+ apiKey: string;
+ dayLimit?: number;
+ tokenLimit?: number;
+ baseURL?: string;
+ /**
+ * 解密密钥
+ */
+ decryptKey?: string;
+};
+
+export type AIConfig = {
+ title?: string;
+ description?: string;
+ models: AIModel[];
+ secretKeys: SecretKey[];
+ permission?: Permission;
+ filter?: {
+ objectKey: string;
+ type: 'array' | 'object';
+ operate: 'removeAttribute' | 'remove';
+ attribute: string[];
+ }[];
+};
+export class AIConfigParser {
+ private config: AIConfig;
+ result: ProviderResult;
+ constructor(config: AIConfig) {
+ this.config = config;
+ }
+ /**
+ * 获取模型配置
+ * @param opts
+ * @returns
+ */
+ getProvider(opts: GetProviderOpts): ProviderResult {
+ const { model, group, decryptKey } = opts;
+ const modelConfig = this.config.models.find((m) => m.model === model && m.group === group);
+ const groupConfig = this.config.secretKeys.find((m) => m.group === group);
+ if (!modelConfig) {
+ throw new Error(`在模型组 ${group} 中未找到模型 ${model}`);
+ }
+ const mergeConfig = {
+ ...modelConfig,
+ ...groupConfig,
+ decryptKey: decryptKey || groupConfig?.decryptKey,
+ };
+ // 验证模型配置
+ if (!mergeConfig.provider) {
+ throw new Error(`模型 ${model} 未配置提供商`);
+ }
+ if (!mergeConfig.model) {
+ throw new Error(`模型 ${model} 未配置模型名称`);
+ }
+ if (!mergeConfig.apiKey) {
+ throw new Error(`组 ${group} 未配置 API 密钥`);
+ }
+ if (!mergeConfig.group) {
+ throw new Error(`组 ${group} 未配置`);
+ }
+ this.result = mergeConfig;
+ return mergeConfig;
+ }
+ /**
+ * 获取解密密钥
+ * @param opts
+ * @returns
+ */
+ async getSecretKey(opts?: {
+ getCache?: (key: string) => Promise;
+ setCache?: (key: string, value: string) => Promise;
+ providerResult?: ProviderResult;
+ }) {
+ const { getCache, setCache, providerResult } = opts || {};
+ const { apiKey, decryptKey, group = '', model } = providerResult || this.result;
+ const cacheKey = `${group}--${model}`;
+ if (!decryptKey) {
+ return apiKey;
+ }
+ if (getCache) {
+ const cache = await getCache(cacheKey);
+ if (cache) {
+ return cache;
+ }
+ }
+ const secretKey = decryptAES(apiKey, decryptKey);
+ if (setCache) {
+ await setCache(cacheKey, secretKey);
+ }
+ return secretKey;
+ }
+ /**
+ * 加密
+ * @param plainText
+ * @param secretKey
+ * @returns
+ */
+ encrypt(plainText: string, secretKey: string) {
+ return encryptAES(plainText, secretKey);
+ }
+ /**
+ * 解密
+ * @param cipherText
+ * @param secretKey
+ * @returns
+ */
+ decrypt(cipherText: string, secretKey: string) {
+ return decryptAES(cipherText, secretKey);
+ }
+
+ /**
+ * 获取模型配置
+ * @returns
+ */
+ getSelectOpts() {
+ const { models, secretKeys = [] } = this.config;
+
+ return models.map((model) => {
+ const selectOpts = secretKeys.find((m) => m.group === model.group);
+ return {
+ ...model,
+ ...selectOpts,
+ };
+ });
+ }
+ getConfig(keepSecret?: boolean, config?: AIConfig) {
+ const chatConfig = config ?? this.config;
+ if (keepSecret) {
+ return chatConfig;
+ }
+ // 过滤掉secret中的所有apiKey,移除掉并返回chatConfig
+ const { secretKeys = [], ...rest } = chatConfig || {};
+ return {
+ ...rest,
+ secretKeys: secretKeys.map((item) => {
+ return {
+ ...item,
+ apiKey: undefined,
+ decryptKey: undefined,
+ };
+ }),
+ };
+ }
+}
diff --git a/src/provider/utils/token.ts b/src/provider/utils/token.ts
new file mode 100644
index 0000000..81ce82c
--- /dev/null
+++ b/src/provider/utils/token.ts
@@ -0,0 +1,34 @@
+import { encoding_for_model, get_encoding } from 'tiktoken';
+
+
+const MODEL_TO_ENCODING = {
+ 'gpt-4': 'cl100k_base',
+ 'gpt-4-turbo': 'cl100k_base',
+ 'gpt-3.5-turbo': 'cl100k_base',
+ 'text-embedding-ada-002': 'cl100k_base',
+ 'text-davinci-002': 'p50k_base',
+ 'text-davinci-003': 'p50k_base',
+} as const;
+
+export function numTokensFromString(text: string, model: keyof typeof MODEL_TO_ENCODING = 'gpt-3.5-turbo'): number {
+ try {
+ // 对于特定模型使用专门的编码器
+ const encoder = encoding_for_model(model);
+ const tokens = encoder.encode(text);
+ const tokenCount = tokens.length;
+ encoder.free(); // 释放编码器
+ return tokenCount;
+ } catch (error) {
+ try {
+ // 如果模型特定的编码器失败,尝试使用基础编码器
+ const encoder = get_encoding(MODEL_TO_ENCODING[model]);
+ const tokens = encoder.encode(text);
+ const tokenCount = tokens.length;
+ encoder.free(); // 释放编码器
+ return tokenCount;
+ } catch (error) {
+ // 如果编码失败,使用一个粗略的估计:平均每个字符0.25个token
+ return Math.ceil(text.length * 0.25);
+ }
+ }
+}
diff --git a/src/test/chunks/01-get.ts b/src/test/chunks/01-get.ts
new file mode 100644
index 0000000..2c00f8d
--- /dev/null
+++ b/src/test/chunks/01-get.ts
@@ -0,0 +1,65 @@
+import { getChunks } from '../../../../../src/provider/utils/chunk.ts';
+
+const str = 'Hello world this is a test 你好沙盒 very big';
+
+
+const str2 = `不能直接使用 tiktoken(OpenAI的分词器)来计算 Qwen 模型的 Token 数量,因为两者的分词规则(Tokenization)和词表(Vocabulary)完全不同。
+
+为什么不能混用?
+词表不同
+
+tiktoken 是 OpenAI 为 GPT 系列设计的(如 gpt-3.5-turbo, gpt-4),其词表针对英语和代码优化。
+
+Qwen 使用独立训练的 BPE 词表,对中文、多语言的支持更友好,分词粒度可能不同。
+
+分词结果差异大
+同一段文本,tiktoken 和 Qwen 的分词结果可能完全不同。例如:
+
+OpenAI (tiktoken): "你好" → ['你', '好'](2 Tokens)
+
+Qwen: "你好" → ['你好'](1 Token,如果词表中包含该组合)
+
+性能问题
+即使强制使用 tiktoken 计算 Qwen 的 Token,结果也不准确,可能导致:
+
+输入超出模型上下文限制(因统计偏差)。
+
+API 计费或本地推理时出现意外错误。
+
+正确方法:用 Qwen 的分词器
+通过 Hugging Face transformers 加载 Qwen 的原生分词器:
+
+python
+复制
+from transformers import AutoTokenizer
+
+# 加载 Qwen 的分词器(以 Qwen-7B 为例)
+tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen-7B", trust_remote_code=True)
+
+text = "你好,Qwen模型!"
+tokens = tokenizer.tokenize(text) # 查看分词结果
+token_count = len(tokenizer.encode(text, add_special_tokens=False))
+
+print("分词结果:", tokens)
+print("Token数量:", token_count)
+常见问题
+为什么需要 trust_remote_code=True?
+Qwen 的分词器是自定义实现的(非 Hugging Face 原生),此参数允许从模型仓库加载运行代码。
+
+其他语言的 Token 计算?
+Qwen 对非英语(如中文、日文)的分词效率较高,但仍需用其原生分词器统计。
+
+与 tiktoken 的速度对比?
+tiktoken 是纯 Python 实现,速度较快;Qwen 的分词器基于 Hugging Face,可能稍慢但对齐模型需求。
+
+总结
+禁止混用:tiktoken ≠ Qwen 分词器。
+
+始终使用模型配套工具:Qwen 需通过 transformers 加载其官方分词器。
+
+中文场景特别注意:Qwen 对中文的分词更高效,直接使用可避免偏差。
+
+如果需要验证分词规则,可通过 tokenizer.vocab 查看词表内容(但注意词表通常较大)。`
+
+const chunks = getChunks(str2);
+console.log(chunks);
diff --git a/src/test/encrypt/index.ts b/src/test/encrypt/index.ts
new file mode 100644
index 0000000..c922ea3
--- /dev/null
+++ b/src/test/encrypt/index.ts
@@ -0,0 +1,9 @@
+import { encryptAES, decryptAES } from '../../../../../src/provider/utils/parse-config.ts';
+
+const plainx = process.env.API_KEY;
+const decryptKey = process.env.DECRYPT_KEY;
+const encrypt = encryptAES(plainx, decryptKey);
+console.log('encrypt', encrypt);
+
+const decrypt = decryptAES(encrypt, decryptKey);
+console.log(decrypt);
diff --git a/src/test/func-call/curl.sh b/src/test/func-call/curl.sh
new file mode 100644
index 0000000..f9f00ae
--- /dev/null
+++ b/src/test/func-call/curl.sh
@@ -0,0 +1,35 @@
+curl --request POST \
+ --url https://api.siliconflow.cn/v1/chat/completions \
+ --header 'Authorization: Bearer sk-qbiigkzoaamuqxtwlgkugodncebkfbosemadfubjrseobpvx' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "model": "Qwen/Qwen3-14B",
+ "messages": [
+ {
+ "role": "user",
+ "content": "计算a+b的值"
+ }
+ ],
+ "stream": false,
+ "max_tokens": 512,
+ "stop": null,
+ "temperature": 0.7,
+ "top_p": 0.7,
+ "top_k": 50,
+ "frequency_penalty": 0.5,
+ "n": 1,
+ "response_format": {
+ "type": "text"
+ },
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "description": "计算a,b,c算法的值,a=1,b=2,c=3",
+ "name": "compouted",
+ "parameters": {},
+ "strict": false
+ }
+ }
+ ]
+}'
\ No newline at end of file
diff --git a/src/test/func-call/demo.ts b/src/test/func-call/demo.ts
new file mode 100644
index 0000000..0cf3f77
--- /dev/null
+++ b/src/test/func-call/demo.ts
@@ -0,0 +1,116 @@
+import { SiliconFlow } from '../../../../../src/provider/chat-adapter/siliconflow.ts';
+import { Ollama } from '../../../../../src/provider/chat-adapter/ollama.ts';
+import dotenv from 'dotenv';
+
+dotenv.config();
+const siliconflow = new SiliconFlow({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ model: 'Qwen/Qwen3-14B',
+});
+const ollama = new Ollama({
+ model: 'qwen3:32b',
+ apiKey: process.env.OLLAMA_API_KEY,
+ baseURL: process.env.OLLAMA_BASE_URL,
+});
+const main = async () => {
+ const usage = await siliconflow.getUsageInfo();
+ console.log(usage);
+};
+// 1. 定义工具函数
+const availableFunctions: Record Promise> = {
+ get_time: async (args: { location: string }) => {
+ // 模拟API调用
+ console.log('time', args);
+ return {
+ time: '2022-03-22 12:00:00',
+ };
+ },
+ get_location: async (args: { symbol: string }) => {
+ // 模拟API调用
+ console.log('location', args);
+ return {
+ city: 'Beijing',
+ };
+ },
+};
+
+// main();
+const funcCall = async (model = siliconflow) => {
+ const tools = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_time',
+ description: '获取当前时间',
+ parameters: {
+ type: 'object',
+ properties: {
+ place: {
+ type: 'string',
+ description: '位置',
+ },
+ },
+ required: ['place'],
+ },
+ },
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'get_location',
+ description: '获取当前位置',
+ // parameters: {},
+ parameters: {},
+ strict: false,
+ },
+ },
+ ];
+ const messages: any[] = [{ role: 'user', content: '获取当前位置的当前时间' }];
+ const res = await model.chat(messages, {
+ tools: tools as any,
+ });
+ console.log(res.choices[0]);
+ const assistantMessage = res.choices[0].message;
+ const finish_reason = res.choices[0].finish_reason;
+ messages.push(assistantMessage);
+ let toolCalls = assistantMessage.tool_calls;
+ console.log("toolCalls", JSON.stringify(toolCalls));
+ let maxRetries = 3;
+ while (toolCalls && toolCalls.length > 0) {
+ // 处理每个函数调用
+ for (const toolCall of toolCalls) {
+ const functionName = toolCall.function.name;
+ const functionArgs = JSON.parse(toolCall.function.arguments);
+ // 调用本地函数
+ const functionResponse = await availableFunctions[functionName](functionArgs);
+ // 将结果添加到消息历史
+ messages.push({
+ role: 'tool',
+ name: functionName,
+ content: JSON.stringify(functionResponse),
+ tool_call_id: toolCall.id,
+ });
+ }
+
+ // 第二次调用 - 将函数结果发送给模型获取最终回复
+ const secondResponse = await model.chat(messages, {
+ tools: tools as any,
+ });
+
+ const finalMessage = secondResponse.choices[0].message;
+ messages.push(finalMessage);
+ const _toolCalls = finalMessage.tool_calls;
+ console.log("toolCalls", JSON.stringify(toolCalls) ,finalMessage.role);
+ toolCalls = _toolCalls ? _toolCalls : [];
+ maxRetries--;
+ if (maxRetries <= 0) {
+ break;
+ }
+
+ console.log('tool calls', toolCalls);
+ }
+
+ console.log(messages);
+};
+
+funcCall(ollama as any);
diff --git a/src/test/model-scope/index.ts b/src/test/model-scope/index.ts
new file mode 100644
index 0000000..ac33319
--- /dev/null
+++ b/src/test/model-scope/index.ts
@@ -0,0 +1,26 @@
+import { ModelScope } from '../../../../../src/provider/chat-adapter/model-scope.ts';
+import { log } from '../../../../../src/logger/index.ts';
+import util from 'util';
+import { config } from 'dotenv';
+config();
+
+const chat = new ModelScope({
+ apiKey: process.env.MODEL_SCOPE_API_KEY,
+ model: 'Qwen/Qwen2.5-Coder-32B-Instruct',
+});
+
+// chat.chat([{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }]);
+const chatMessage = [{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }];
+
+const main = async () => {
+ const res = await chat.test();
+ log.info('test', res);
+};
+
+main();
+const mainChat = async () => {
+ const res = await chat.chat(chatMessage as any);
+ log.info('chat', res);
+};
+
+// mainChat();
diff --git a/src/test/ollama-knowledge.ts b/src/test/ollama-knowledge.ts
new file mode 100644
index 0000000..de514ed
--- /dev/null
+++ b/src/test/ollama-knowledge.ts
@@ -0,0 +1,37 @@
+import { Knowledge } from '../../../../src/provider/knowledge/knowledge.ts';
+import fs from 'fs';
+import dotenv from 'dotenv';
+
+dotenv.config();
+const knowledge = new Knowledge({
+ embeddingModel: 'bge-m3:latest',
+ baseURL: 'https://ollama.xiongxiao.me/v1',
+ model: 'qwq:latest',
+ apiKey: process.env.OLLAMA_API_KEY,
+});
+
+const main = async () => {
+ const res = await knowledge.generateEmbeddingCore('Hello world this is a test 你好沙盒 very big');
+ fs.writeFileSync('docs/embedding.json', JSON.stringify(res, null, 2));
+ console.log(res);
+};
+
+main();
+
+const main2 = async () => {
+ const text1 = 'Hello, world! this is a test';
+ const text2 = 'Hello, world! this is a test 2';
+ const text3 = 'Hello, world! this is a test 3';
+ const text4 = 'Hello, world! this is a test 4';
+ const text5 = 'Hello, world! this is a test 5';
+ const text6 = 'Hello, world! this is a test 6';
+ const text7 = 'Hello, world! this is a test 7';
+ const text8 = 'Hello, world! this is a test 8';
+ const text9 = 'Hello, world! this is a test 9';
+ const text10 = 'Hello, world! this is a test 10';
+ const res = await knowledge.generateEmbeddingCore([text1, text2, text3, text4, text5, text6, text7, text8, text9, text10]);
+ fs.writeFileSync('docs/embedding2.json', JSON.stringify(res, null, 2));
+ console.log(res);
+};
+
+// main2();
diff --git a/src/test/ollama.ts b/src/test/ollama.ts
new file mode 100644
index 0000000..c71658d
--- /dev/null
+++ b/src/test/ollama.ts
@@ -0,0 +1,86 @@
+import { Ollama } from '../../../../src/provider/chat-adapter/ollama.ts';
+import util from 'util';
+const chat = new Ollama({
+ baseURL: 'https://ollama.xiongxiao.me/v1',
+ apiKey: 'xiongxiao2233',
+ model: 'qwq:latest',
+});
+
+// chat.chat([{ role: 'user', content: 'Hello, world!' }]);
+
+const main = async () => {
+ const res = await chat.test();
+ console.log(util.inspect(res, { depth: null, colors: true }));
+};
+
+// main();
+
+const getJson = async () => {
+ const res = await chat.chat(
+ [
+ { role: 'system', content: '把发送的数据,返回给我对应的json,只处理完发送的数据。如果发送了多个,给我一个数组' },
+ // { role: 'user', content: '{"name":"John","age":30}' },
+ { role: 'user', content: 'name: 张三' },
+ { role: 'user', content: 'name: 李四, age: 18' },
+ ],
+ {
+ response_format: {
+ type: 'json_schema',
+ json_schema: {
+ name: 'user',
+ description: '用户信息',
+ schema: {
+ type: 'object',
+ // properties: {
+ // name: { type: 'string' },
+ // // age: { type: 'number' },
+ // },
+ // // required: ['name', 'age'],
+ // required: ['name'],
+ properties: {
+ name: { type: 'string' },
+ age: { type: 'number' },
+ },
+ required: ['name', 'age'],
+ },
+ },
+ },
+ n: 10,
+ },
+ );
+ console.log(util.inspect(res, { depth: null, colors: true }));
+};
+
+// getJson();
+
+const createChat1 = async () => {
+ const res = await chat.chat(
+ [
+ { role: 'user', content: 'a=1, b=2, c=3' },
+ { role: 'user', content: 'a+b+c=?' },
+ { role: 'assistant', content: '给定的值为 \\( a = 1 \\), \\( b = 2 \\), \\( c = 3 \\)。\n' + '\n' + '因此,\\( a + b + c = 1 + 2 + 3 = 6 \\)。' },
+ { role: 'user', content: 'a+b+c+4=?' },
+ ],
+ {
+ model: 'qwen2.5:7b',
+ },
+ );
+ console.log(util.inspect(res, { depth: null, colors: true }));
+};
+
+// createChat1();
+
+const getTags = async () => {
+ const res = await chat.listModels();
+ console.log(util.inspect(res, { depth: null, colors: true }));
+};
+
+// getTags();
+
+const getRunModels = async () => {
+ const res = await chat.listRunModels();
+ console.log('current', new Date().toISOString());
+ console.log(util.inspect(res, { depth: null, colors: true }));
+};
+
+// getRunModels();
diff --git a/src/test/provider/index.ts b/src/test/provider/index.ts
new file mode 100644
index 0000000..872c7b7
--- /dev/null
+++ b/src/test/provider/index.ts
@@ -0,0 +1,7 @@
+import { ProviderManager } from '../../../../../src/provider/index.ts';
+import { config } from 'dotenv';
+config();
+const providerConfig = { provider: 'ModelScope', model: 'Qwen/Qwen2.5-Coder-32B-Instruct', apiKey: process.env.MODEL_SCOPE_API_KEY };
+const provider = await ProviderManager.createProvider(providerConfig);
+const result = await provider.chat([{ role: 'user', content: '你好' }]);
+console.log(result);
diff --git a/src/test/siliconflow/common.ts b/src/test/siliconflow/common.ts
new file mode 100644
index 0000000..7c5d817
--- /dev/null
+++ b/src/test/siliconflow/common.ts
@@ -0,0 +1,13 @@
+import { SiliconFlow } from '../../../../../src/provider/chat-adapter/siliconflow.ts';
+import { KnowledgeBase } from '../../../../../src/provider/knowledge/knowledge-base.ts';
+export const siliconflow = new SiliconFlow({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ model: 'Qwen/Qwen2-7B-Instruct',
+});
+
+export const knowledge = new KnowledgeBase({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ baseURL: SiliconFlow.BASE_URL,
+ model: 'Qwen/Qwen2-7B-Instruct',
+ embeddingModel: 'Pro/BAAI/bge-m3',
+});
diff --git a/src/test/siliconflow/get.ts b/src/test/siliconflow/get.ts
new file mode 100644
index 0000000..727d0c7
--- /dev/null
+++ b/src/test/siliconflow/get.ts
@@ -0,0 +1,22 @@
+import { SiliconFlow } from '../../../../../src/provider/chat-adapter/siliconflow.ts';
+import dotenv from 'dotenv';
+
+dotenv.config();
+const siliconflow = new SiliconFlow({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ model: 'Qwen/Qwen2-7B-Instruct',
+});
+
+
+const main = async () => {
+ const usage = await siliconflow.getUsageInfo();
+ console.log(usage);
+};
+
+main();
+const mainChat = async () => {
+ const res = await siliconflow.chat([{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }]);
+ console.log(res);
+};
+
+// mainChat();
diff --git a/src/test/siliconflow/knowledge/create.ts b/src/test/siliconflow/knowledge/create.ts
new file mode 100644
index 0000000..57c038e
--- /dev/null
+++ b/src/test/siliconflow/knowledge/create.ts
@@ -0,0 +1,18 @@
+import { knowledge } from '../common.ts';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+// 包含: 9184 个汉字 953 个标点(全角) 2493 个字母 52 个数字
+const content = fs.readFileSync(path.join(__dirname, 'qwen.md'), 'utf-8');
+const text = 'Hello, world';
+const main = async () => {
+ const res = await knowledge.generateEmbeddingCore([content, content]);
+ console.log(res);
+ // 8000 tokens 大概1w个字 2万个字符
+ console.log('speak', knowledge.getChatUsage());
+};
+main();
diff --git a/src/test/siliconflow/knowledge/qwen.md b/src/test/siliconflow/knowledge/qwen.md
new file mode 100644
index 0000000..1ec9c30
--- /dev/null
+++ b/src/test/siliconflow/knowledge/qwen.md
@@ -0,0 +1,232 @@
+# Qwen的基本背景与概述
+
+Qwen是由通义实验室研发的超大规模语言模型,具备强大的语言理解和生成能力,能够胜任多种自然语言处理任务。作为一款先进的AI语言模型,Qwen不仅能够回答问题、撰写文本、进行逻辑推理,还支持多语言交流、代码编写、创意写作等多种应用场景。其核心目标是为用户提供高效、智能的语言交互体验,同时满足企业级应用的需求。
+
+Qwen的技术基础建立在深度学习和大规模预训练模型之上,借助海量文本数据进行训练,使其能够精准理解语义并生成高质量的文本。该模型采用了先进的Transformer架构,结合自注意力机制和位置编码,使其在处理长文本、复杂语境和多轮对话时表现出色。此外,Qwen经过多轮优化,支持高效的推理和生成,能够在不同计算环境下稳定运行,包括云端、本地服务器乃至边缘设备。
+
+在功能方面,Qwen的核心能力涵盖自然语言理解(NLU)、自然语言生成(NLG)和对话交互等多个领域。它能够准确解析用户的意图,并根据上下文生成符合逻辑、流畅自然的回应。同时,Qwen支持多语言处理,能够理解和生成包括中文、英文、法语、西班牙语等多种语言的文本,满足全球化应用场景的需求。此外,Qwen还具备代码生成能力,能够理解和编写多种编程语言的代码,为开发者提供智能辅助。
+
+Qwen的应用场景广泛,涵盖智能客服、内容创作、教育辅助、数据分析、科学研究等多个领域。例如,在智能客服领域,Qwen可以充当虚拟助手,自动回答用户咨询,提高服务效率;在内容创作方面,它可以协助撰写新闻报道、营销文案、剧本等,提高创作效率;在教育领域,Qwen可以作为个性化学习助手,帮助学生解答问题、提供学习建议。此外,Qwen还可用于自动化报告生成、法律文书分析、金融数据分析等专业领域,为企业提供智能化解决方案。
+
+总体而言,Qwen是一款集强大语言处理能力、多语言支持和广泛适用性于一体的先进语言模型。随着人工智能技术的不断发展,Qwen将继续优化其性能,拓展更多应用场景,为用户提供更智能、更高效的交互体验。
+
+## Qwen的技术架构与核心组件
+
+Qwen的技术架构基于深度学习模型,尤其是Transformer架构,这是当前最先进的自然语言处理(NLP)模型之一。Qwen的设计目标是实现高效的文本理解和生成能力,使其能够胜任各种复杂的语言任务,包括问答、文本摘要、对话交互、代码生成等。为了达到这一目标,Qwen依赖于大规模的训练数据、复杂的参数体系以及高效的训练和推理机制,从而确保其在不同应用场景下的卓越表现。
+
+### 1. **Transformer架构:Qwen的基础模型**
+
+Qwen的核心模型基于Transformer架构,这是一种采用自注意力机制(Self-Attention)和位置编码(Positional Encoding)的深度学习模型。传统的循环神经网络(RNN)和长短时记忆网络(LSTM)在处理长文本时存在一定的局限性,而Transformer通过自注意力机制能够同时关注整个输入序列的不同部分,从而更有效地捕捉上下文信息。
+
+在Qwen中,Transformer架构被优化以支持大规模参数和高效计算。模型的编码器(Encoder)和解码器(Decoder)由多个堆叠的Transformer层组成,每一层都包含多头注意力(Multi-Head Attention)和前馈神经网络(Feed-Forward Network)。这种结构使Qwen能够并行处理大量信息,提高训练和推理的效率,同时增强对复杂语义关系的理解能力。
+
+### 2. **训练数据:构建Qwen的语言知识库**
+
+Qwen的训练数据来源于互联网上的大量文本,包括网页、书籍、百科、新闻、论文、代码库等。这些数据涵盖了广泛的领域和语言风格,使Qwen能够掌握丰富的知识和语言模式。为了确保训练数据的质量,Qwen的训练过程采用了严格的清洗和过滤机制,以去除低质量、重复或含有噪声的数据。
+
+此外,Qwen的训练数据还包括多语言文本,使其能够支持多种语言的处理和生成。这种多语言能力不仅有助于提升模型的泛化能力,也使其在国际化应用场景中具备更强的适应性。通过大规模的训练数据,Qwen能够学习到丰富的语言结构、词汇关系和语义模式,从而在实际应用中提供更加准确和自然的文本生成能力。
+
+### 3. **参数规模:Qwen的模型复杂度**
+
+Qwen的参数规模是其强大语言能力的关键因素之一。参数数量决定了模型的表达能力和学习能力,Qwen的参数量级达到了超大规模,使其能够处理复杂的语言任务。大规模参数使得Qwen在面对不同的输入时能够更准确地理解上下文,并生成符合语义逻辑的输出。
+
+为了优化计算效率,Qwen采用了模型压缩和分布式训练技术。这些技术使得Qwen能够在不同的计算环境下高效运行,包括云端服务器、本地计算机甚至边缘设备。此外,Qwen还支持动态调整参数规模,以适应不同的计算资源和应用场景,从而在性能和效率之间取得平衡。
+
+### 4. **训练与推理机制:Qwen的优化策略**
+
+Qwen的训练过程采用了大规模分布式训练策略,利用多个GPU或TPU并行计算,以加快训练速度并提高模型收敛效率。此外,Qwen还结合了混合精度训练(Mixed Precision Training)和梯度累积(Gradient Accumulation)等技术,以减少内存占用并提升训练稳定性。
+
+在推理阶段,Qwen支持多种解码策略,包括贪婪解码(Greedy Decoding)、束搜索(Beam Search)和采样解码(Sampling Decoding),以确保生成文本的多样性和准确性。此外,Qwen还引入了缓存机制,以加速多轮对话中的推理过程,使其在实时交互应用中表现更加流畅。
+
+总体而言,Qwen的技术架构融合了先进的Transformer模型、大规模训练数据、超大规模参数体系以及高效的训练和推理策略。这一架构使其在自然语言处理任务中具备强大的表现力,并能够适应不同应用场景的需求。
+
+## Qwen的核心模块及其功能
+
+Qwen的核心功能由多个关键模块共同支撑,包括自然语言理解(NLU)、自然语言生成(NLG)和对话交互模块。这些模块各自承担不同的任务,同时相互协作,使Qwen能够高效地处理复杂的语言任务,并提供流畅、准确的交互体验。
+
+### 1. **自然语言理解(NLU):解析用户输入的语义**
+
+自然语言理解(NLU)模块是Qwen的基础组成部分,负责解析用户的输入文本,理解其语义和意图。该模块利用深度学习技术,特别是基于Transformer的模型,对输入文本进行编码,提取关键信息,如实体识别、情感分析、意图分类等。例如,当用户输入“明天北京天气如何?”时,NLU模块能够识别出“天气”作为核心主题,并提取“明天”和“北京”作为时间与地点信息。
+
+NLU模块的核心任务是将用户的自然语言输入转换为结构化数据,以便后续模块能够基于这些信息进行处理。例如,在智能客服应用中,NLU模块能够识别用户的问题类型(如订单查询、产品咨询等),并将其分类,以指导后续的回复生成。此外,NLU模块还支持多语言理解,使其能够在不同的语言环境中准确解析用户意图。
+
+### 2. **自然语言生成(NLG):生成高质量的文本输出**
+
+自然语言生成(NLG)模块负责根据NLU模块解析的语义信息生成自然、流畅的文本输出。该模块基于深度学习模型,结合上下文信息和语法规则,生成符合用户需求的文本。例如,在回答“明天北京天气如何?”这一问题时,NLG模块会结合天气预报数据,生成诸如“明天北京天气晴朗,最高气温25摄氏度”的回答。
+
+NLG模块的核心任务是确保生成的文本既准确又自然,符合人类语言的表达习惯。为此,该模块采用了多种技术,如注意力机制(Attention Mechanism)和语言模型微调(Fine-tuning),以提升生成文本的质量。此外,NLG模块还支持个性化生成,能够根据不同用户的偏好调整表达方式。例如,在内容创作场景中,Qwen可以根据用户的需求生成不同风格的文本,如正式报告、轻松的对话式文本或富有创意的文学作品。
+
+### 3. **对话交互模块:实现多轮对话与上下文理解**
+
+对话交互模块是Qwen的重要组成部分,负责管理多轮对话,并确保对话的连贯性和一致性。该模块利用上下文记忆机制,记录用户的历史对话信息,从而在多轮对话中提供更精准的响应。例如,如果用户在第一轮对话中询问“北京有哪些旅游景点?”,并在第二轮对话中继续提问“那故宫的门票价格是多少?”,对话交互模块能够识别“故宫”指的是北京的景点,并提供相应的信息。
+
+对话交互模块的核心功能包括上下文理解、对话状态追踪(Dialogue State Tracking)和对话策略优化(Dialogue Policy Optimization)。这些功能使Qwen能够在复杂的对话环境中保持连贯性,并提供更加智能的交互体验。此外,该模块还支持强化学习(Reinforcement Learning),通过不断优化对话策略,提高Qwen在不同场景下的对话能力。
+
+### 4. **模块之间的协同作用**
+
+Qwen的NLU、NLG和对话交互模块紧密协作,共同完成复杂的语言任务。NLU模块负责解析用户输入,提取关键信息;NLG模块基于这些信息生成自然流畅的文本;对话交互模块则确保多轮对话的连贯性和逻辑性。这种模块化设计使Qwen能够在不同的应用场景中提供高效、智能的语言交互体验,无论是在智能客服、内容创作还是个性化推荐等领域,都能展现出卓越的能力。
+
+## Qwen的典型应用场景
+
+Qwen作为一款功能强大的超大规模语言模型,已经在多个领域展现出广泛的应用潜力。从内容创作到智能客服,再到教育辅助和科学研究,Qwen的多功能性使其成为各类行业的理想工具。以下将详细介绍Qwen在这些典型场景中的具体应用及其优势。
+
+### 1. **内容创作:提升文本生成效率**
+
+Qwen在内容创作领域具有极高的实用性,能够帮助用户高效生成高质量的文本内容。无论是新闻报道、市场营销文案、社交媒体内容,还是创意写作,Qwen都能提供智能化的辅助。
+
+在新闻写作方面,Qwen可以基于给定的关键词或事件,快速生成结构清晰、逻辑严谨的新闻稿件。例如,在体育赛事报道中,Qwen可以自动整理比赛数据、分析比赛过程,并生成符合新闻格式的报道,大幅减少人工撰写的时间成本。
+
+在广告与营销文案创作方面,Qwen能够根据品牌调性和目标受众,生成富有吸引力的营销内容。例如,针对特定产品,Qwen可以自动撰写产品介绍、促销文案或社交媒体推文,提高营销活动的效率。此外,Qwen还能根据用户反馈不断优化文案风格,使其更贴合市场需求。
+
+在创意写作方面,Qwen可以作为作者的智能助手,提供灵感支持和内容优化。例如,在小说创作过程中,Qwen可以根据故事情节生成合理的对白、描述场景或补充细节,帮助作家克服写作瓶颈。此外,Qwen还能协助剧本创作,提供角色设定、情节发展建议,甚至生成完整的剧本草稿。
+
+### 2. **智能客服:优化客户交互体验**
+
+Qwen在智能客服领域的应用尤为突出,能够显著提升客户服务的效率和质量。传统的人工客服往往面临响应时间长、服务覆盖范围有限等问题,而Qwen能够提供全天候、个性化的智能客服解决方案。
+
+在在线客服系统中,Qwen可以充当虚拟助手,自动回答用户的常见问题。例如,在电商平台,Qwen可以处理订单查询、退换货政策咨询、支付问题等,提供即时、准确的答复,减少人工客服的负担。此外,Qwen能够根据用户的历史对话记录,提供个性化推荐,如推荐符合用户偏好的商品,提升用户体验。
+
+在电话客服系统中,Qwen可以作为语音助手,与用户进行自然的对话交互。例如,在银行或电信服务中,Qwen能够自动处理账户查询、业务办理、账单支付等操作,减少用户等待时间,提高服务效率。此外,Qwen还能分析用户的语音情绪,识别潜在的投诉或不满,并提供相应的安抚建议,优化客户关系管理。
+
+### 3. **教育辅助:个性化学习与智能教学**
+
+Qwen在教育领域的应用同样具有广阔的前景,能够为学生、教师和教育机构提供智能化的教学辅助工具。
+
+在个性化学习方面,Qwen可以充当智能学习助手,根据学生的学习进度和薄弱环节,提供个性化的学习建议。例如,在数学学习过程中,Qwen可以自动分析学生的错题,推荐相关练习题,并提供详细的解题步骤,帮助学生巩固知识点。此外,Qwen还能根据学生的学习风格,调整讲解方式,使其更符合个人需求。
+
+在语言学习方面,Qwen可以充当智能语言导师,提供语法纠正、发音评估和口语练习等功能。例如,在英语学习过程中,Qwen可以模拟真实的对话场景,与用户进行互动,纠正语法错误,并提供地道的表达建议。此外,Qwen还能根据用户的学习目标,推荐适合的阅读材料,提高语言学习的效率。
+
+在教学辅助方面,Qwen可以帮助教师优化教学内容。例如,教师可以使用Qwen自动生成课程讲义、测试题或教学案例,提高备课效率。此外,Qwen还能分析学生的学习数据,帮助教师发现班级的整体学习趋势,并提供针对性的教学改进方案。
+
+### 4. **科学研究:加速学术研究与数据分析**
+
+Qwen在科学研究领域的应用也日益广泛,能够为研究人员提供高效的信息检索、文献分析和实验辅助工具。
+
+在学术写作方面,Qwen可以协助研究人员撰写论文、报告和综述文章。例如,Qwen可以根据研究主题自动生成论文摘要、文献综述或研究背景,提高写作效率。此外,Qwen还能帮助研究人员查找相关文献,分析研究趋势,并提供数据可视化建议,使研究成果更易于理解和传播。
+
+在数据分析方面,Qwen可以协助研究人员处理大规模文本数据。例如,在社会科学研究中,Qwen可以自动分析社交媒体文本、调查问卷或新闻报道,提取关键信息,并生成结构化数据,便于后续分析。此外,Qwen还能结合机器学习技术,进行情感分析、主题建模等任务,提高研究的智能化水平。
+
+在实验设计与优化方面,Qwen能够提供科学实验的辅助建议。例如,在生物医学研究中,Qwen可以分析实验数据,提供优化实验方案的建议。此外,Qwen还能帮助研究人员设计实验变量、预测实验结果,并提供统计分析支持,提高实验的科学性和可重复性。
+
+总体而言,Qwen凭借其强大的语言理解和生成能力,在内容创作、智能客服、教育辅助和科学研究等多个领域展现出卓越的应用价值。随着人工智能技术的不断发展,Qwen的应用场景将进一步拓展,为各行各业提供更加智能化的解决方案。
+
+## Qwen的训练与优化方法
+
+Qwen的训练与优化是其高性能表现的关键所在。作为一个超大规模语言模型,Qwen的训练过程涉及庞大的数据集、复杂的训练流程以及多种优化策略,以确保模型在不同应用场景下的高效性和准确性。以下将详细介绍Qwen的训练方法,包括数据预处理、训练流程和优化策略,并探讨其面临的挑战及可能的解决方案。
+
+### 1. **数据预处理:构建高质量的训练数据集**
+
+Qwen的训练数据来源于互联网上的海量文本,涵盖网页、书籍、百科、新闻、论文、代码库等多个领域。为了确保训练数据的质量,Qwen的训练过程采用了严格的数据预处理步骤,包括数据清洗、去重、语言过滤和格式标准化。
+
+首先,Qwen的训练数据需要经过清洗,以去除低质量或无效内容。例如,训练数据中的拼写错误、乱码、重复文本以及含有噪声的网页内容都会影响模型的学习效果。为此,Qwen采用了基于规则和机器学习的数据清洗技术,以自动识别并剔除低质量数据。
+
+其次,去重处理是训练数据预处理的重要环节。由于互联网上的文本数据存在大量重复内容,Qwen使用了高效的文本相似度计算方法,如MinHash和SimHash,以检测并去除高度相似的文本,确保训练数据的多样性和有效性。
+
+此外,Qwen的训练数据包含多语言文本,因此需要进行语言过滤和格式标准化。Qwen采用语言识别模型(Language Identification Model)来自动识别文本的语言,并根据不同的语言制定相应的预处理规则。例如,对于中文文本,Qwen会进行分词处理,而对于英文文本,则采用标准的词干化(Stemming)和词形还原(Lemmatization)技术,以统一文本格式并提高模型的泛化能力。
+
+### 2. **训练流程:大规模分布式训练与优化**
+
+Qwen的训练流程采用了大规模分布式训练策略,以加速模型训练并提高计算效率。由于Qwen的参数规模庞大,传统的单机训练方式难以满足计算需求,因此Qwen采用了基于GPU和TPU的分布式训练架构,利用多个计算节点并行处理数据。
+
+在训练过程中,Qwen采用了分阶段训练策略,包括预训练(Pre-training)和微调(Fine-tuning)。预训练阶段的目标是让模型学习通用的语言表示能力,通常采用自监督学习方法,如掩码语言建模(Masked Language Modeling, MLM)和下一句预测(Next Sentence Prediction, NSP)。在这一阶段,Qwen会学习大量的文本模式,并建立强大的语言理解能力。
+
+微调阶段则是在特定任务上进一步优化模型性能。例如,在对话生成任务中,Qwen会使用对话数据进行微调,以提高模型在多轮对话中的连贯性和逻辑性。此外,Qwen还采用了强化学习(Reinforcement Learning, RL)技术,以优化对话交互模块的表现,使其能够根据用户反馈不断调整生成策略。
+
+为了提高训练效率,Qwen还采用了混合精度训练(Mixed Precision Training)和梯度累积(Gradient Accumulation)等优化策略。混合精度训练通过使用半精度浮点数(FP16)进行计算,以减少内存占用并加快训练速度,而梯度累积则允许在较小的批量(Batch Size)下进行训练,从而减少计算资源的消耗。
+
+### 3. **优化策略:提高模型性能与推理效率**
+
+Qwen的优化策略主要集中在模型压缩、推理加速和多任务学习等方面,以确保模型在实际应用中的高效性和可扩展性。
+
+首先,模型压缩技术被广泛应用于Qwen,以降低模型的计算成本。例如,Qwen采用了知识蒸馏(Knowledge Distillation)技术,通过使用一个较小的学生模型来模仿大模型的行为,从而在保持较高性能的同时减少计算资源的消耗。此外,Qwen还采用了量化(Quantization)技术,将模型的浮点数参数转换为低精度整数,以进一步减少模型的存储和计算需求。
+
+其次,Qwen在推理阶段采用了高效的解码策略,以提高文本生成的速度。例如,Qwen支持束搜索(Beam Search)和采样解码(Sampling Decoding)等方法,以在生成文本时平衡多样性和准确性。此外,Qwen还引入了缓存机制,以加速多轮对话中的推理过程,使其在实时交互应用中表现更加流畅。
+
+最后,Qwen采用了多任务学习(Multi-Task Learning)策略,以提高模型的泛化能力。Qwen在训练过程中同时学习多个相关任务,如文本分类、命名实体识别、问答系统等,使模型能够更好地适应不同的应用场景。这种策略不仅提高了模型的性能,还减少了针对特定任务进行微调的需求,从而降低了训练和部署成本。
+
+### 4. **挑战与可能的解决方案**
+
+尽管Qwen的训练与优化方法已经取得了显著成果,但仍面临一些挑战。例如,训练数据的质量和多样性仍然存在一定的不确定性,可能导致模型出现偏差或过度拟合。此外,大规模模型的训练和推理成本较高,限制了其在资源受限环境下的应用。
+
+为了解决这些问题,Qwen未来可能会采用更加精细的数据筛选机制,以确保训练数据的多样性和公平性。此外,Qwen还可以探索更加高效的模型架构,如稀疏训练(Sparse Training)和自适应计算(Adaptive Computation),以进一步降低计算成本并提高模型的可扩展性。
+
+## Qwen与其他主流语言模型的对比
+
+在当前的人工智能领域,Qwen与GPT系列、Claude等主流语言模型相比,具备独特的技术特点和优势。以下将从功能、性能、应用场景和技术创新等方面进行对比分析,以突出Qwen的核心竞争力。
+
+### 1. **功能与多语言支持**
+
+Qwen在功能上与GPT系列和Claude类似,均具备强大的自然语言理解和生成能力,能够进行多轮对话、文本摘要、问答系统、代码生成等任务。然而,Qwen在多语言支持方面具有独特优势。相比于GPT-4主要专注于英文环境,Qwen不仅支持中文,还涵盖了100多种其他语言,使其在国际化应用场景中更具适应性。此外,Qwen在中文理解与生成方面表现出更强的准确性,得益于其大规模的中文训练数据,使其在中文语境下能够提供更自然、流畅的交互体验。
+
+相比之下,Claude 3在多语言支持上也有一定的能力,但其在代码生成和中文处理方面不如Qwen。Qwen的代码生成能力经过专门优化,能够理解和编写多种编程语言,如Python、Java、C++等,这使其在开发者社区中具备较高的实用性。而GPT-4虽然也支持代码生成,但其训练数据主要来源于英文环境,导致在中文代码解析和生成方面存在一定的局限性。
+
+### 2. **性能与计算效率**
+
+在模型性能方面,Qwen采用了高效的训练和推理优化策略,使其在大规模计算环境中具备更高的吞吐量和更低的延迟。Qwen支持混合精度训练和模型压缩技术,如知识蒸馏和量化,能够在保持较高性能的同时降低计算资源的消耗。这种优化策略使得Qwen在云端和边缘设备上均能高效运行,而GPT-4和Claude 3在资源消耗上相对较高,尤其是在处理长文本或多轮对话时,计算需求较大。
+
+此外,Qwen的推理加速技术,如缓存机制和高效的解码策略(如束搜索和采样解码),使其在实时交互场景中表现更加流畅。相比之下,GPT-4和Claude 3在推理速度上略逊于Qwen,尤其是在高并发请求的情况下,可能会出现响应延迟。
+
+### 3. **应用场景与行业适配性**
+
+Qwen的应用场景广泛,尤其在中文互联网生态中具有较强的适配性。例如,在智能客服、内容创作、教育辅助和科研数据分析等领域,Qwen能够提供高度定制化的解决方案。相比之下,GPT-4主要面向英文用户和国际企业,虽然在英语环境下的应用较为成熟,但在中文市场上的本地化支持相对较弱。Claude 3则在隐私保护和安全性方面具有优势,适用于金融、法律等对数据敏感的行业,但其在中文处理和代码生成方面的能力不如Qwen。
+
+此外,Qwen在代码生成和开发者工具方面的优化使其在软件工程领域具有较强的应用价值。例如,Qwen能够自动编写代码、调试错误并提供优化建议,而GPT-4和Claude 3虽然也支持代码生成,但其在特定编程语言上的优化程度不及Qwen。
+
+### 4. **技术创新与模型迭代**
+
+Qwen在技术创新方面也展现出独特的优势。例如,Qwen采用了强化学习(Reinforcement Learning)技术,使其在多轮对话和交互式任务中能够不断优化自身的回复策略。此外,Qwen的多模态能力也在不断扩展,未来有望支持图像和视频理解,进一步提升其在多媒体交互场景中的表现。相比之下,GPT-4和Claude 3在多模态支持方面仍处于早期阶段,尚未完全整合视觉和语言模型的能力。
+
+此外,Qwen的模型迭代速度较快,能够快速响应用户反馈并进行优化。例如,Qwen的版本更新通常涵盖性能优化、功能增强和安全性提升,而GPT-4和Claude 3的更新周期较长,且主要依赖于核心模型的改进,缺乏灵活的定制化调整能力。
+
+综上所述,Qwen在多语言支持、代码生成、计算效率、行业适配性和技术创新等方面均展现出独特的优势。相较于GPT系列和Claude等主流模型,Qwen不仅具备强大的语言处理能力,还在中文生态和开发者社区中具有更强的适用性,使其成为当前AI语言模型领域的重要竞争者。
+
+## Qwen的未来发展与潜在方向
+
+随着人工智能技术的不断进步,Qwen在未来的发展将围绕几个核心方向展开,包括功能增强、技术优化和行业应用的进一步拓展。这些发展方向不仅将提升Qwen的性能和适用性,也将推动其在更多领域的深度融合和创新应用。
+
+### 1. **多模态能力的扩展**
+
+当前,Qwen主要专注于文本处理,但未来的发展方向之一是增强其多模态能力,使其能够同时理解和处理文本、图像、音频和视频等多种形式的信息。这一能力的提升将使Qwen在人机交互、智能助手和内容生成等场景中发挥更大的作用。例如,在智能客服领域,Qwen可以结合语音识别和图像分析技术,提供更加自然和高效的交互体验;在教育领域,Qwen可以解析教材中的图文内容,并提供个性化的学习建议。此外,Qwen的多模态能力还可以应用于内容创作,使其能够根据图像或视频生成相关的文本描述,提高创作效率和质量。
+
+### 2. **领域适应与垂直应用优化**
+
+尽管Qwen已经具备广泛的应用能力,但未来的发展将更加注重特定行业的垂直优化。例如,在医疗领域,Qwen可以结合医学知识库,提供专业的诊断辅助和健康咨询;在法律领域,Qwen可以优化法律文书的自动生成和案例分析能力,提高法律工作者的效率;在金融领域,Qwen可以增强对市场数据的分析能力,提供智能投资建议和风险预测。通过针对不同行业的数据和需求进行微调,Qwen将在各个垂直领域提供更加精准和高效的解决方案。
+
+### 3. **推理与逻辑能力的提升**
+
+当前,Qwen在自然语言理解和生成方面表现出色,但在复杂推理和逻辑分析方面仍有提升空间。未来的发展方向之一是增强Qwen的推理能力,使其能够处理更复杂的逻辑任务,如数学计算、科学推导和因果推理。这一能力的提升将使Qwen在科研、工程设计和决策支持等场景中发挥更大的作用。例如,在科学研究中,Qwen可以协助研究人员进行假设验证和数据分析;在企业决策中,Qwen可以提供基于逻辑推理的商业策略建议。此外,Qwen还可以结合强化学习技术,使其在交互过程中不断优化自身的推理策略,提高回答的准确性和逻辑性。
+
+### 4. **跨语言与全球化的进一步优化**
+
+虽然Qwen已经支持100多种语言,但未来的发展将进一步优化其跨语言能力,使其在不同语言之间的转换和理解更加自然和准确。例如,Qwen可以增强对小语种的支持,使其在全球范围内提供更加均衡的语言服务。此外,Qwen可以优化跨语言的对话交互能力,使其在多语言环境中提供更加流畅的翻译和交流体验。这一能力的提升将使Qwen在国际化企业、跨文化交流和全球教育等领域发挥更大的作用。
+
+### 5. **模型轻量化与部署优化**
+
+为了适应不同的计算环境,Qwen的未来发展方向还包括模型轻量化和部署优化。当前,Qwen的模型规模较大,主要适用于云端计算,但未来的发展目标是使其能够在本地设备和边缘计算环境中高效运行。例如,通过模型压缩、知识蒸馏和量化技术,Qwen可以在手机、平板电脑和IoT设备上提供高效的推理能力,而无需依赖云端计算资源。此外,Qwen还可以优化其推理速度和内存占用,使其在实时交互和高并发场景中表现更加稳定和高效。
+
+随着这些发展方向的推进,Qwen将在人工智能领域发挥更加重要的作用,为各行各业提供更加智能和高效的解决方案。
+
+## Qwen的技术优势与行业影响
+
+Qwen凭借其先进的技术架构和强大的功能,在人工智能领域展现出卓越的表现。其基于Transformer的深度学习模型结合大规模训练数据和超大规模参数体系,使其在自然语言理解、生成和对话交互等方面达到行业领先水平。Qwen不仅能够精准解析用户意图,还能生成高质量、符合语境的文本,支持多轮对话和个性化交互。此外,Qwen的代码生成能力、多语言支持和高效推理优化使其在开发者社区和国际化应用场景中具备独特优势。
+
+在实际应用中,Qwen已经广泛应用于内容创作、智能客服、教育辅助、科学研究等多个领域。在内容创作方面,Qwen能够自动生成新闻报道、营销文案、剧本等,提高创作效率;在智能客服领域,Qwen能够提供全天候的自动化应答,提升客户体验;在教育领域,Qwen可以作为个性化学习助手,帮助学生答疑解惑,优化教学方案;在科研领域,Qwen能够辅助学术写作、数据分析和实验设计,加速科研进程。这些应用不仅提升了各行业的效率,也推动了人工智能技术的普及和落地。
+
+Qwen的成功离不开其强大的技术基础和持续的优化策略。其训练数据覆盖广泛的文本来源,结合数据清洗、去重和语言过滤技术,确保模型的泛化能力。在训练过程中,Qwen采用大规模分布式训练、混合精度训练和梯度累积等优化策略,提高训练效率并减少计算资源消耗。此外,Qwen的推理加速技术,如缓存机制和高效的解码策略,使其在实时交互场景中表现更加流畅。这些技术优化不仅提升了Qwen的性能,也为其在不同计算环境下的部署提供了灵活性。
+
+与其他主流语言模型相比,Qwen在多语言支持、代码生成和计算效率方面展现出独特优势。相较于GPT系列和Claude等模型,Qwen在中文处理和本地化应用方面更加精准,同时支持多种编程语言的代码生成,使其在开发者社区中具有更高的实用性。此外,Qwen的模型压缩和轻量化技术使其在资源受限环境下仍能高效运行,而GPT-4和Claude 3在推理速度和计算资源消耗方面相对较高。这些技术优势使Qwen在人工智能领域占据重要地位,并推动其在更多行业的深入应用。
+
+展望未来,Qwen的发展方向将围绕多模态能力扩展、垂直行业优化、推理能力增强和全球化部署优化展开。Qwen将进一步提升其跨语言能力,使其在全球范围内提供更加均衡的语言服务。此外,Qwen的模型轻量化和部署优化将使其在本地设备和边缘计算环境中高效运行,为实时交互和高并发场景提供稳定的解决方案。随着人工智能技术的持续演进,Qwen将在内容创作、智能客服、教育、科研等领域发挥更大作用,为各行各业提供更加智能、高效的AI解决方案。
+
+
+## Qwen的技术优势与行业影响
+
+Qwen凭借其先进的技术架构和强大的功能,在人工智能领域展现出卓越的表现。其基于Transformer的深度学习模型结合大规模训练数据和超大规模参数体系,使其在自然语言理解、生成和对话交互等方面达到行业领先水平。Qwen不仅能够精准解析用户意图,还能生成高质量、符合语境的文本,支持多轮对话和个性化交互。此外,Qwen的代码生成能力、多语言支持和高效推理优化使其在开发者社区和国际化应用场景中具备独特优势。
+
+在实际应用中,Qwen已经广泛应用于内容创作、智能客服、教育辅助、科学研究等多个领域。在内容创作方面,Qwen能够自动生成新闻报道、营销文案、剧本等,提高创作效率;在智能客服领域,Qwen能够提供全天候的自动化应答,提升客户体验;在教育领域,Qwen可以作为个性化学习助手,帮助学生答疑解惑,优化教学方案;在科研领域,Qwen能够辅助学术写作、数据分析和实验设计,加速科研进程。这些应用不仅提升了各行业的效率,也推动了人工智能技术的普及和落地。
+
+Qwen的成功离不开其强大的技术基础和持续的优化策略。其训练数据覆盖广泛的文本来源,结合数据清洗、去重和语言过滤技术,确保模型的泛化能力。在训练过程中,Qwen采用大规模分布式训练、混合精度训练和梯度累积等优化策略,提高训练效率并减少计算资源消耗。此外,Qwen的推理加速技术,如缓存机制和高效的解码策略,使其在实时交互场景中表现更加流畅。这些技术优化不仅提升了Qwen的性能,也为其在不同计算环境下的部署提供了灵活性。
+
+与其他主流语言模型相比,Qwen在多语言支持、代码生成和计算效率方面展现出独特优势。相较于GPT系列和Claude等模型,Qwen在中文处理和本地化应用方面更加精准,同时支持多种编程语言的代码生成,使其在开发者社区中具有更高的实用性。此外,Qwen的模型压缩和轻量化技术使其在资源受限环境下仍能高效运行,而GPT-4和Claude 3在推理速度和计算资源消耗方面相对较高。这些技术优势使Qwen在人工智能领域占据重要地位,并推动其在更多行业的深入应用。
+
+展望未来,Qwen的发展方向将围绕多模态能力扩展、垂直行业优化、推理能力增强和全球化部署优化展开。Qwen将进一步提升其跨语言能力,使其在全球范围内提供更加均衡的语言服务。此外,Qwen的模型轻量化和部署优化将使其在本地设备和边缘计算环境中高效运行,为实时交互和高并发场景提供稳定的解决方案。随着人工智能技术的持续演进,Qwen将在内容创作、智能客服、教育、科研等领域发挥更大作用,为各行各业提供更加智能、高效的AI解决方案。
diff --git a/src/test/siliconflow/videos/index.ts b/src/test/siliconflow/videos/index.ts
new file mode 100644
index 0000000..d06a9c6
--- /dev/null
+++ b/src/test/siliconflow/videos/index.ts
@@ -0,0 +1,100 @@
+import { SiliconFlow } from '../../../../../../src/provider/chat-adapter/siliconflow.ts';
+import { VideoSiliconFlow } from '../../../../../../src/provider/media/video/siliconflow.ts';
+import dotenv from 'dotenv';
+import fs from 'fs';
+import path from 'path';
+import Stream from 'stream';
+
+dotenv.config();
+const siliconflow = new SiliconFlow({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ model: 'Qwen/Qwen2-7B-Instruct',
+});
+const videoSiliconflow = new VideoSiliconFlow({
+ apiKey: process.env.SILICONFLOW_API_KEY,
+ model: 'Qwen/Qwen2-7B-Instruct',
+});
+
+const main = async () => {
+ const usage = await siliconflow.getUsageInfo();
+ console.log(usage);
+};
+
+// main();
+const mainChat = async () => {
+ const test2=`我永远记得那个改变一切的下午。十八岁生日后的第三天,我正坐在自家后院的老橡树杈上,用平板电脑调试我最新设计的森林动物追踪程序。我的红发——妈妈总说像"燃烧的枫叶"——在午后的阳光下泛着铜色的光泽,有几缕不听话的发丝被微风拂过我的脸颊。
+
+"芮薇!"妈妈的声音从厨房窗口传来,"外婆发来加密信息,说需要你马上过去一趟。"
+
+我差点从树上掉下来。外婆从不发加密信息——除非情况紧急。作为退休的网络安全专家,外婆一直教导我"过度谨慎总比后悔莫及"。`
+ try {
+ const res = await siliconflow.openai.audio.speech.create({
+ model: 'FunAudioLLM/CosyVoice2-0.5B',
+ // voice: 'FunAudioLLM/CosyVoice2-0.5B:diana',
+ voice: 'speech:test:h36jngt7ms:zarwclhblfjfyonslejr',
+ // input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始',
+ // input: '这是一个新的轮回,非常有趣的故事。',
+ input: test2,
+ response_format: 'mp3',
+ });
+
+ console.log(res);
+
+ const dir = path.join(process.cwd(), 'videos');
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+
+ const filePath = path.join(dir, `output-${Date.now()}.mp3`);
+
+ // 假设 res 是一个可读流
+ if (res instanceof Stream.Readable) {
+ const writeStream = fs.createWriteStream(filePath);
+ res.pipe(writeStream);
+
+ return new Promise((resolve, reject) => {
+ writeStream.on('finish', () => {
+ console.log('文件已保存至:', filePath);
+ resolve(filePath);
+ });
+ writeStream.on('error', reject);
+ });
+ }
+ // 假设 res 是一个 ArrayBuffer 或 Buffer
+ else if (res.arrayBuffer) {
+ const buffer = Buffer.from(await res.arrayBuffer());
+ fs.writeFileSync(filePath, buffer);
+ console.log('文件已保存至:', filePath);
+ return filePath;
+ }
+ // 假设 res 是一个包含 blob 的对象
+ else if (res.blob) {
+ // @ts-ignore
+ const buffer = Buffer.from(res.blob, 'base64');
+ fs.writeFileSync(filePath, buffer);
+ console.log('文件已保存至:', filePath);
+ return filePath;
+ } else {
+ throw new Error('无法识别的响应格式');
+ }
+ } catch (error) {
+ console.error('保存音频文件时出错:', error);
+ throw error;
+ }
+};
+
+mainChat();
+
+const vidioUpload = async () => {
+ const filePath = path.join(process.cwd(), 'videos', 'my_speech_text.mp3');
+ const fileBuffer = fs.readFileSync(filePath);
+ const fileBase64 = 'data:audio/mpeg;base64,' + fileBuffer.toString('base64');
+ console.log('fileBase64', fileBase64.slice(0, 100));
+ const fileBlob = new Blob([fileBuffer], { type: 'audio/wav' });
+ const file = new File([fileBlob], 'my_speech_text.mp3', { type: 'audio/mp3' });
+ const res = await videoSiliconflow.uploadAudioVoice(file);
+ // console.log('vidioUpload', res);
+ // uri:speech:test:h36jngt7ms:zarwclhblfjfyonslejr
+ return res;
+};
+// vidioUpload();
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..b4d458c
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "@kevisual/types/json/backend.json",
+ "compilerOptions": {
+ "baseUrl": "./",
+ "allowImportingTsExtensions": true,
+ "paths": {
+ "@/*": [
+ "src/*"
+ ]
+ }
+ },
+ "include": [
+ "src/**/*.ts",
+ ],
+ "exclude": [],
+}
\ No newline at end of file
diff --git a/videos/my_speech_text.mp3 b/videos/my_speech_text.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..42f2ba966121f65331c60ee6e45f4e4b38dc2780
GIT binary patch
literal 20420
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
x7Ga^1kPLs>L%A76R#!x
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