aichat-core
B站视频:【实现】仿CherryStudio做一个Web端全能AI助手(!!!全网第一个讲思路和实现细节的UP!!!)
npm仓库如下:
配套脚手架如下:
1. 介绍
NPM仓库地址:www.npmjs.com/package/aic…简化 LLM 与 MCP 集成的前端核心库(TypeScript)
aichat-core
是一个前端核心库,旨在显著降低在项目中集成 OpenAI 和 MCP 服务的复杂度。它封装了 openai-sdk
和 mcp-sdk
,提供了:
核心业务模型与流程抽象:预定义了关键业务模型,并实现了结合 MCP 协议的流式聊天核心逻辑。
开箱即用的 UI 组件:封装了常用交互组件,包括:
HTML 代码的实时编辑与预览
Markdown 内容的优雅渲染
支持多轮对话展示的消息项组件
核心价值:开发者只需专注于后端业务逻辑实现和前端 UI 定制,而无需深入处理 LLM 与 MCP 之间复杂的交互细节。aichat-core
为您处理了底层的复杂性,让您更快地构建基于 LLM 和 MCP 的智能对话应用!
2. 使用
2.1 依赖项
install该模块,请注意安装以下对应版本的npm包
"@modelcontextprotocol/sdk": "^1.15.0", "codemirror": "5", "github-markdown-css": "^5.8.1", "openai": "^5.3.0", "react-codemirror2": "^8.0.1", "react-json-view": "^1.21.3", "react-markdown": "^9.1.0", "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0"
暴露出来的核心功能:
/** TS类型及核心业务实现客户端类封装 */import LLMCallBackMessage from "./core/response/LLMCallBackMessage";import LLMCallBackMessageChoice from "./core/response/LLMCallBackMessageChoice";import LLMCallBackToolMessage from "./core/response/LLMCallBackToolMessage";import LLMUsage from "./core/response/LLMUsage";import LLMThinkUsage from "./core/response/LLMThinkUsage";import LLMClient from "./core/LLMClient";import McpClient from "./core/McpClient";import McpTool from "./core/McpTool";import LLMUtil from "./core/LLMUtil";import ChatMarkDown from "./ui/chat-markdown";import CodeEditorPreview from "./ui/code-editor-preview";import ChatBubbleItem from "./ui/chat-bubble-item";export { LLMCallBackMessage, LLMCallBackMessageChoice, LLMCallBackToolMessage, LLMThinkUsage, LLMUsage, LLMClient, McpClient, McpTool, LLMUtil, ChatMarkDown, CodeEditorPreview, ChatBubbleItem,};
2.2 安装
npm install aichat-core && yarn add aichat-core
2.3 核心业务模型
LLMCallBackMessage.ts
/** AI响应消息回调对象 */export default interface LLMCallBackMessage { /** 消息ID,一定是有的 */ id: string | number; /** 消息创建时间,一定有的,只不过初始化的时候无,UI回显需要, 案例:1750923621 */ timed?: number; /** 使用的llm模型,一定是有的 */ model: string; /** 消息角色(system、assistant、user、tool)*/ role: string; /** 集成antd design x Bubble的typing属性,true表示设置聊天内容打字动画,false则不使用 */ typing?: boolean; /** 集成antd design x Bubble的loading属性,true表示聊天内容加载,false表示不加载 */ loading?: boolean; /** 消息组,一次LLM响应可能包含多组消息,初始化的时候可空 */ choices?: LLMCallBackMessageChoice[];}
LLMCallBackToolMessage
/** 回调消息选择对象 */export default interface LLMCallBackMessageChoice { /** 消息索引,从1开始(多轮对话,index是累加的) */ index: number; /** 正常响应内容,一定有,就算LLM择取工具时,content也是有值的,只不过是空串 */ content: string; /** 推理内容,不一定有,要看LLM是否具备 */ reasoning_content?: string; /** * -1 或 undefined :不带推理 * 0:思考中 * 1:思考结束 */ thinking?: number; /** 回调工具消息数组 */ tools?: LLMCallBackToolMessage[]; /** 消耗的token统计 */ usage?: LLMMessageUsage;}
LLMCallBackToolMessage
/** 工具调用消息回调对象 */export default interface LLMCallBackToolMessage { /** 调用id */ id:string; /** 函数的索引 */ index: number; /** 函数的名称 */ name: string; /** 函数的参数,这个选填,如果能弄过来更好,便于后续错误调试 */ arguments?: any; /** 函数调用的结果内容 */ content?: string;}
LLMClient.ts
/** * @description 用于与 OpenAI 和 MCP 服务进行交互,支持流式聊天和工具调用。 * @author appleyk * @github https://github.com/kobeyk * @date 2025年7月9日21:44:02 */export default class LLMClient { // 定义常量类型(防止魔法值) public static readonly CONTENT_THINKING = "thinking"; public static readonly TYPE_FUNCTION = "function"; public static readonly TYPE_STRING = "string"; public static readonly REASON_STOP = "stop"; public static readonly REASON_TOO_CALLS = "tool_calls"; public static readonly ROLE_AI = "assistant"; public static readonly ROLE_SYSTEM = "system"; public static readonly ROLE_USER = "user"; public static readonly ROLE_TOOL = "tool"; public static readonly THINGKING_START_TAG = "<think>"; public static readonly THINGKING_END_TAG = "</think>"; // llm大模型对象 llm: OpenAI; // mcp客户端对象(二次封装),不一定有,一旦有,必须初始化和进行"三次握手"后才可以正常使用mcp服务端提供的能力 mcpClient?: McpClient; // 模型名称 modelName: string; // mcpServer服务器连接地址 mcpServer = ""; // mcp工具列表 tools: McpTool[] = []; // 初始化mcp的状态,默认false,如果开启llm流式聊天前,这个状态为false,则报错 initMcpState = false; // 思考状态(每一次消息问答结束后,thinkState要回归false) thinkState = false; /** * 构造器 * @param mcpServer mcp服务器连接地址(目前仅支持streamable http,后续再放开stdio和sse),如果空的话,则表示外部不使用mcp * @param mcpClientName mcpClient名称 * @param apiKey llm访问接口对应的key,如果没有,填写"EMPTY",默认值走系统配置,如需要指定外部传进来 * @param baseUrl llm访问接口的地址,目前走的是openai标准,大部分llm都支持,除了少数,后续可能要支持下(再说),默认值走系统配置,如需要指定外部传进来 * @param modelName llm模型的名称,这个需要发起llm聊天的时候指定,默认值走系统配置,如需要指定外部传进来 */ constructor( mcpServer = "", mcpClientName = "", apiKey:string, baseUrl:string, modelName:string ) { // 使用llm,必须要实例化OpenAI实例对象 this.llm = new OpenAI({ apiKey: apiKey, baseURL: baseUrl, dangerouslyAllowBrowser: true, // web端直接调用openai是不安全的,这个要开启 }); // 使用llm,必须要指定模型的名称 this.modelName = modelName; // 表示使用mcp来为llm提供外部工具调用参考 if (mcpServer !== "") { this.mcpServer = mcpServer; this.mcpClient = new McpClient(mcpClientName); } } /** * 流式聊天升级版(支持多轮对话) * @param messages 消息上下文(多轮多话要把上一轮对话的结果加进去) * @param onContentCallBack 文本内容回调函数 * @param onCallToolsCallBack 工具回调函数,如果传入空列表,则表示一轮工具调用结束 * @param onCallToolResultCallBack 工具调用执行结果回调函数,name->result键值对 * @param callBackMessage 回调消息大对象(包含的信息相当全) * @param controllerRef 请求中断控制器 * @param loop 对话轮数 */ async chatStreamLLMV2( messages: ChatCompletionMessageParam[], onContentCallBack: (callBackMessage: LLMCallBackMessage) => void, onCallToolsCallBack: (_toolCalls: LLMStreamChoiceDeltaTooCall[]) => void, onCallToolResultCallBack: (name: string, result: any) => void, callBackMessage: LLMCallBackMessage, controllerRef: AbortController | null, loop: number = 1 // 轮数 ) { const stream = await this.genLLMStream(messages, controllerRef); if (stream && typeof stream[Symbol.asyncIterator] === LLMClient.TYPE_FUNCTION) { let _toolCalls: LLMStreamChoiceDeltaTooCall[] = []; for await (const chunk of stream) { /** 如果usage包含且内容不等于null,则说明本次结束 */ callBackMessage.timed = chunk.created; callBackMessage.model = chunk.model; let usage = chunk.usage; if (usage) { let _choices = callBackMessage.choices if (_choices && _choices.length > 0) { let _choiceMessage = this.filterChoiceMessage(callBackMessage, loop); _choiceMessage.index = loop; _choiceMessage.thinking = 2; _choiceMessage.reasoning_content = ""; _choiceMessage.content = ""; _choiceMessage.usage = { completion_tokens: usage.completion_tokens, prompt_tokens: usage.prompt_tokens, total_tokens: usage.total_tokens } let newCallBackMessage = JSON.parse(JSON.stringify(callBackMessage)) onContentCallBack(newCallBackMessage); } break; } /** 判断chunk.choices[0]是否有finish_reason字段,如果有赋值 */ let finishReason = chunk.choices[0].finish_reason ?? ""; if (finishReason !== "") { await this.dealFinishReason( finishReason, _toolCalls, messages, onContentCallBack, onCallToolsCallBack, onCallToolResultCallBack, callBackMessage, controllerRef, loop ); } let toolCalls = chunk.choices[0].delta.tool_calls; /** 如果工具有值,则构建工具列表 */ if (toolCalls && toolCalls.length > 0) { this.dealToolCalls(toolCalls, _toolCalls); } else { /** 否则流式展示消息内容 */ const { choices } = chunk; await this.dealTextContent(choices, onContentCallBack, callBackMessage, loop); } } } else { console.error("Stream is not async iterable."); } }}
2.4 aichat-core使用案例
核心集成代码片段如下:
// 存储会话列表(默认default-0)const [conversations, setConversations] = useState( DEFAULT_CONVERSATIONS_ITEMS); // 历史消息,一个对话对应一组历史消息const [messageHistory, setMessageHistory] = useState<Record<string, any>>(useMockData > 0 ? mockHistoryMessages : []);...// 实例化llmClient及初始化mcp服务(如果有mcp server 服务支撑的话)llmClient = new LLMClient(mcpServer, "demo", apiKey, baseUrl, modelName);await llmClient.initMcpServer();.../** 消息回调 (callBackMessage对象已经经过深拷贝)*/const onMessageContentCallBack = (callBackMessage: LLMCallBackMessage) => { const _currentConversation = curConversation.current; let _llmChoices = callBackMessage.choices; let _messageId = callBackMessage.id; if (!_llmChoices || _llmChoices.length == 0) { return []; } setMessageHistory((sPrev) => ({ ...sPrev, [_currentConversation]: sPrev[_currentConversation].map((message: LLMCallBackMessage) => { if (_messageId != message.id) { return message; } let _message: LLMCallBackMessage = { id: _messageId, timed: callBackMessage.timed, model: callBackMessage.model, role: callBackMessage.role, typing: true, loading: false, } /** 拿到原来的 */ let preMessageChoices = message.choices ?? []; _llmChoices.forEach((updateChoice: LLMCallBackMessageChoice) => { let _targets = preMessageChoices.filter((preChoice: any) => updateChoice.index == preChoice.index) ?? [] // 有值就动态修改值 if (_targets.length > 0) { let _target = _targets[0]; let _reason_content = _target.reasoning_content ?? ""; let _content = _target.content ?? ""; _target.thinking = updateChoice.thinking; if (updateChoice.reasoning_content && updateChoice.reasoning_content != "") { _target.reasoning_content = _reason_content + updateChoice.reasoning_content; } if (updateChoice.content && updateChoice.content != "") { _target.content = _content + updateChoice.content; } _target.tools = updateChoice.tools; _target.usage = updateChoice.usage; } else { // 否则的话就添加一个 preMessageChoices.push(updateChoice) } }) // 最后别忘了给值(这个地方要用深拷贝) _message.choices = JSON.parse(JSON.stringify(preMessageChoices)); return _message; }), }));};...// 发起流式聊天await llmClient.current.chatStreamLLMV2( messages, onMessageContentCallBack, onCallToolCallBack, onCallToolResultCallBack, onInitCallBackMessage(messageId), abortControllerRef);...