LLM外部工具调用演示

一个轻量级的Node.js演示项目,展示如何使用OpenAI函数调用功能让大型语言模型(LLM)在适当的上下文管理下使用外部工具。

  • by dragonlong206
  • JavaScript
  • 本地部署
  • 5 星标
  • 更新于
在 GitHub 查看 dragonlong206/mcp-demo

使用说明

🧩 MCP演示(模型+工具+上下文)

使用OpenAI函数调用的MCP的轻量级Node.js演示,展示LLM如何使用具有适当上下文管理的外部工具。

📦 项目结构

mcp-demo/
├── package.json
├── src/
│   ├── app.js                          # Entry point
│   ├── llm/
│   │   └── openai.js                   # OpenAI Chat Completions API
│   ├── mcp/
│   │   ├── context.js                  # Context builder + tool result enrichment
│   │   ├── orchestrator.js             # MCP main workflow (runMCP)
│   │   ├── executor.js                 # Executes validated tool calls
│   │   └── toolRegistry.js             # Tool list → OpenAI tool schemas
│   ├── tools/
│   │   ├── revenueTool.js              # get_revenue function
│   │   └── timeTool.js                 # get_time function
│   └── utils/
│       └── logger.js                   # Logging utilities
└── README.md

🚀 快速开始

1.安装依赖项

npm install

2.设置OpenAI API密钥

export OPENAI_API_KEY=your_api_key_here

3.运行演示

演示1

node src/app.js "What is the revenue for 02/2025?"

预期产量:

LLM回复:

{
  "index": 0,
  "message": {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_",
        "type": "function",
        "function": {
          "name": "get_revenue",
          "arguments": {
            "month": "02/2025"
          }
        }
      }
    ],
    "refusal": null,
    "annotations": []
  },
  "logprobs": null,
  "finish_reason": "tool_calls"
}

工具调用后的LLM响应:

{
  "index": 0,
  "message": {
    "role": "assistant",
    "content": "The revenue for February 2025 is 150,000.",
    "finish_reason": "stop"
  },
  ...
}

最终答案:

The revenue for February 2025 is 150,000.

🧠 核心组件

src/mcp/context.js

管理上下文信息和工具结果丰富:

  • buildInitialContext({ userId, locale, appState })

    • 使用用户元数据、业务规则和工具使用示例构建系统消息
    • 返回LLM指令的区域设置感知上下文
  • enrichConversationWithToolResult(conversation, { toolName, result, toolCallId })

    • 将工具响应附加到对话历史记录中
    • 为多回转工具的使用保持适当的信息链
  • resolveLocaleText(locale, key)

    • 检索特定于区域设置的消息(越南语、英语)
    • 支持新语言的可扩展性

src/mcp/orchestrator.js

主MCP工作流程(runMCP):

  1. 构建上下文 -使用用户区域设置和业务规则初始化系统消息
  2. 致电LLM -使用可用工具发送用户查询
  3. 检查工具调用 -如果LLM想要使用工具:
    • 提取工具名称和参数
    • 执行工具
    • 将工具结果附加到对话中
    • 再次调用LLM并返回结果
  4. 返回答案 -LLM的最终文本回复

src/mcp/executor.js

工具执行管道:

  • 从注册表中检索工具定义
  • 验证Zod模式的参数
  • 执行工具处理程序
  • 返回结果或错误

src/mcp/toolRegistry.js

工具管理:

  • 维护所有可用工具的列表
  • 将工具定义转换为与OpenAI兼容的模式
  • 按名称提供工具查找

src/tools/revenueTool.js

收入数据工具示例:

{
  name: "get_revenue",
  description: "Get revenue for a specific month in MM/YYYY format",
  schema: z.object({
    month: z.string().describe("Month in MM/YYYY format (e.g., '02/2025')")
  }),
  handler: async ({ month }) => { /* returns revenue data */ }
}

src/llm/openai.js

OpenAI集成:

  • 配置GPT-4o-mini型号
  • 处理工具架构格式
  • 管理函数调用流

🧪 演示使用示例

示例1:按月查询收入

node src/app.js "What is the revenue for 02/2025?"

流量:

  • LLM标识月份 02/2025 查询
  • 呼叫 get_revenue 和 {"month":"02/2025"}
  • 返回格式化收入

示例2:当前时间查询

node src/app.js "What is today's date?"

流量:

  • LLM识别日期查询并建议呼叫 get_time 工具
  • 呼叫 get_time 工具
  • 返回当前日期响应

示例3:当月收入查询(多个工具调用)

node src/app.js "What is the revenue for this month?"

预期:

  • LLM建议致电 get_time 获取当前月份的工具
  • 呼叫 get_time 工具,返回当月。LLM建议致电 get_revenue 带有当月参数的工具
  • 返回当月收入

🛠️ MCP的工作原理:流程

User Input
    ↓
[1] buildInitialContext
    ↓
[2] callLLM with tools
    ↓
    └─→ LLM response: tool_calls?
         ├─ YES: [3] executeTool
         │        ↓
         │        [4] enrichConversationWithToolResult
         │        ↓
         │        [2] callLLM again (with tool result)
         │
         └─ NO: Return final_response

🛡️ 主要特点

✅ 上下文注入

系统上下文包括:

  • 用户区域设置和首选项
  • 业务规则(例如,月格式要求)
  • 工具使用示例
  • 元数据(用户ID、环境)

✅ 模式验证

  • Zod模式强制类型安全
  • 执行前拒绝了无效参数
  • 清除错误消息

✅ 工具结果反馈循环

  • 工具输出自动附加到对话中
  • LLM可以对工具结果进行推理
  • 支持多回转刀具使用

✅ 多语言支持

  • 越南语和英语提示
  • 可扩展的区域设置解析器
  • 按区域设置执行业务规则

🔧 扩展演示

添加新工具

  1. 创建 src/tools/myTool.js:
import { z } from "zod";

export const myTool = {
  name: "my_function",
  description: "What this tool does",
  schema: z.object({
    param1: z.string().describe("Parameter description"),
  }),
  handler: async ({ param1 }) => {
    return { result: "data" };
  },
};
  1. 注册于 src/mcp/toolRegistry.js:
import { myTool } from "../tools/myTool.js";

export const tools = [revenueTool, timeTool, myTool];

改进月度验证

在 revenueTool.js,添加正则表达式验证:

schema: z.object({
  month: z
    .string()
    .regex(/^(0[1-9]|1[0-2])\/\d{4}$/, "Format must be MM/YYYY")
    .describe("Month in MM/YYYY format"),
})

连接真实数据库

修改 revenueTool.handler:

handler: async ({ month }) => {
  const data = await db.query("SELECT * FROM revenue WHERE month = ?", [month]);
  return { month, revenue: data[0].revenue };
}

添加身份验证

在 orchestrator.js:

const context = buildInitialContext({
  userId: req.user.id,
  locale: req.user.locale,
  appState: { environment: "prod" }
});

📊 架构图

┌─────────────────────────────────────────────────────┐
│  User Query (Vietnamese/English)                    │
└──────────────────┬──────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────────────────┐
│  orchestrator.js: runMCP()                          │
│  - buildInitialContext()                            │
│  - Add system message + user query                  │
└──────────────────┬──────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────────────────┐
│  openai.js: callLLM()                               │
│  - Send to GPT-4o-mini with tool schemas            │
└──────────────────┬──────────────────────────────────┘
                   ↓
         ┌─────────┴─────────┐
         ↓                   ↓
   [Tool Calls]        [Direct Answer]
         ↓                   ↓
    executor.js          RETURN
  - Resolve tool
  - Validate args
  - Execute handler
         ↓
    enrichConversationWithToolResult()
         ↓
    callLLM() again
         ↓
    Return final answer

🧾 故障排除

错误: Invalid input expected string, received undefined

原因: LLM未提供所需参数
解决方案: 用清晰的例子提高系统提示

错误: Invalid parameter: messages with role 'tool' must be a response to preceeding message with tool_calls

原因: 消息链断开,工具消息没有匹配的助手消息
解决方案: 确保 enrichConversationWithToolResult 被调用时具有完整的对话历史记录,包括助理消息

错误: 401 Unauthorized 调用OpenAI时

原因: API密钥无效或丢失
解决方案:

export OPENAI_API_KEY=sk-...
echo $OPENAI_API_KEY  # verify it's set

LLM不使用工具

原因: 工具架构格式不正确或示例不清楚
解决方案:

  • 检查中的Zod模式转换 toolRegistry.js
  • 在中添加明确的工具使用示例 context.js 系统消息
  • 验证OpenAI请求中是否包含工具名称

📝 建议的后续步骤

  • []添加持久对话历史记录(数据库)
  • []为API调用实现速率限制
  • []添加工具执行的监控/日志记录
  • []为MCP流创建集成测试
  • []添加TypeScript以确保类型安全
  • []记录每次工具调用的成本估算
  • []为失败的工具调用实现重试逻辑
  • []添加对流式响应的支持

🏗️ 技术栈

  • 运行时间: Node.js v24+
  • LLM OpenAI GPT-4o-mini
  • 架构验证: 萨德
  • 环境: Dotenv。

📄 许可证

麻省理工学院


🤝 贡献

欢迎投稿!遵循以下模式:

  • 使用Zod进行模式定义
  • 保持工具处理程序的纯净和无副作用
  • 测试工具输出与预期模式匹配
  • 用新的工具示例更新README

制作❤️ 用于MCP演示