Skip to content

Node 基础 ​

🎯 学习目标

  • 使用 Node 20+、npm/pnpm 管理依赖与脚本
  • 掌握 ESM(import)、fetch、async/await 与 .env 配置
  • 能读懂第二章 Lab、Next.js API Route 与第三章 MCP 示例
  • 理解流式 ReadableStream 与 SSE 解析的基本写法

引言 ​

非主线选修,但本课程 第二章 API 实战、Next.js 流式前端、第三章 MCP Server(TypeScript/Node) 均大量出现 Node。有 Python 背景的同学建议 45–60 分钟 速览本节,然后边做 实战 · 流式 API 边查。

全栈常见分工:Node/Next.js 暴露 /api/chat、SSE 推流到浏览器;Python 跑 RAG 索引与 LangGraph(见 Python 基础)。

章节正文 ​

第 1 步:环境与包管理 ​

bash
# 建议 Node 20 LTS(与 VitePress、MCP SDK 一致)
node -v    # v20.x
npm -v

# 用 nvm / fnm 切换版本(推荐)
# nvm install 20 && nvm use 20

mkdir my-agent-api && cd my-agent-api
npm init -y
npm pkg set type=module          # 启用 ESM,下文 import 才能用
npm install openai dotenv
npm install -D typescript @types/node   # 可选,MCP/大型项目建议 TS
工具说明
npmNode 自带,本课程默认
pnpm省磁盘、monorepo 友好,命令与 npm 类似(pnpm add)
package.json依赖、scripts、是否 "type": "module"
node_modules/安装产物,勿提交 Git

常用脚本:

json
{
  "type": "module",
  "scripts": {
    "chat": "node chat.mjs",
    "dev": "node --watch chat.mjs"
  }
}

第 2 步:ESM 与 CommonJS 区别 ​

本课程 新示例一律 ESM:

javascript
// chat.mjs 或 package.json 含 "type": "module" 的 chat.js
import OpenAI from 'openai'
import 'dotenv/config'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

旧项目可能是 CommonJS(require)。MCP TypeScript SDK 为 ESM-only(见 3.3),新建 MCP Server 请直接用 ESM + "type": "module"。

第 3 步:环境变量与配置 ​

根目录 .env(勿提交 Git):

OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini
LLM_PROVIDER=openai

读取:

javascript
import 'dotenv/config'

const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) throw new Error('Missing OPENAI_API_KEY')

const model = process.env.OPENAI_MODEL ?? 'gpt-4o-mini'

Next.js 中 服务端 用 process.env;客户端只能读 NEXT_PUBLIC_ 前缀变量——永远不要把 API Key 暴露到浏览器。

第 4 步:异步、fetch 与 OpenAI SDK ​

与 2.1 API 与本地部署 对齐:

javascript
import OpenAI from 'openai'
import 'dotenv/config'

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
})

const response = await client.chat.completions.create({
  model: process.env.OPENAI_MODEL ?? 'gpt-4o-mini',
  messages: [
    { role: 'system', content: '你是简洁的中文技术助手。' },
    { role: 'user', content: '用一句话解释 Token。' },
  ],
  max_tokens: 200,
})

console.log(response.choices[0].message.content)
console.log('usage:', response.usage)

原生 fetch(无 SDK、调试 curl 等价物):

javascript
const res = await fetch(`${process.env.OPENAI_BASE_URL}/chat/completions`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: 'hello' }],
  }),
  signal: AbortSignal.timeout(60_000),
})

if (!res.ok) {
  const text = await res.text()
  throw new Error(`upstream ${res.status}: ${text}`)
}

const data = await res.json()
console.log(data.choices[0].message.content)

第 5 步:Next.js App Router API Route ​

Lab 1 的标准形态(服务端代理 Key):

javascript
// app/api/chat/route.js
import OpenAI from 'openai'

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function POST(req) {
  try {
    const { messages } = await req.json()
    const completion = await client.chat.completions.create({
      model: process.env.OPENAI_MODEL ?? 'gpt-4o-mini',
      messages,
      temperature: 0.7,
    })
    return Response.json({
      reply: completion.choices[0].message.content,
      usage: completion.usage,
    })
  } catch (err) {
    console.error(err)
    return Response.json({ error: 'upstream_failed' }, { status: 502 })
  }
}

前端只请求 /api/chat,不接触 Key。

第 6 步:流式输出与 SSE 解析 ​

2.4 SSE 与流式输出 核心模式:

javascript
const res = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: '写一首短诗' }],
  stream: true,
})

for await (const chunk of res) {
  const delta = chunk.choices[0]?.delta?.content
  if (delta) process.stdout.write(delta)
}

Next.js 流式 Response(简化示意):

javascript
export async function POST(req) {
  const { messages } = await req.json()
  const upstream = await fetch(url, {
    method: 'POST',
    headers: { /* ... */ },
    body: JSON.stringify({ model: 'gpt-4o-mini', messages, stream: true }),
  })

  const stream = new ReadableStream({
    async start(controller) {
      const reader = upstream.body.getReader()
      const decoder = new TextDecoder()
      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        const text = decoder.decode(value)
        // 解析 data: {...} 行,提取 delta.content 再 enqueue 给前端
        controller.enqueue(value)
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream' },
  })
}

AbortController 停止生成:

javascript
const ac = new AbortController()
fetch('/api/chat', { method: 'POST', body, signal: ac.signal })
// 用户点「停止」:ac.abort()

第 7 步:文件系统与 JSON ​

javascript
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'

const configPath = path.join(process.cwd(), 'config.json')
const raw = await readFile(configPath, 'utf8')
const config = JSON.parse(raw)

config.version = 2
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8')

MCP / 工具开发常用 同步 读小文件(启动时加载 schema):

javascript
import { readFileSync, existsSync } from 'node:fs'

if (existsSync('./tools.json')) {
  const tools = JSON.parse(readFileSync('./tools.json', 'utf8'))
}

第 8 步:错误处理与 Provider 抽象 ​

javascript
export async function chatWithRetry(client, params, { retries = 1 } = {}) {
  for (let i = 0; i <= retries; i++) {
    try {
      return await client.chat.completions.create(params)
    } catch (err) {
      const status = err.status ?? err.response?.status
      if (status === 429 && i < retries) {
        await new Promise((r) => setTimeout(r, 2000 * (i + 1)))
        continue
      }
      if (status === 401) throw new Error('Invalid API key')
      throw err
    }
  }
}

多 Provider 配置(与 2.1 一致):

javascript
// config/models.mjs
export const providers = {
  openai: {
    baseURL: 'https://api.openai.com/v1',
    apiKey: process.env.OPENAI_API_KEY,
    defaultModel: 'gpt-4o-mini',
  },
  ollama: {
    baseURL: 'http://localhost:11434/v1',
    apiKey: 'ollama',
    defaultModel: 'qwen2.5:7b',
  },
}

export function createClient(name = 'openai') {
  const p = providers[name]
  if (!p?.apiKey) throw new Error(`Missing provider: ${name}`)
  return new OpenAI({ apiKey: p.apiKey, baseURL: p.baseURL })
}

第 9 步:TypeScript 与 MCP 开发提示 ​

MCP Server(3.3–3.4)常用 TypeScript + ESM:

json
// tsconfig.json 片段
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true,
    "outDir": "dist"
  }
}

要点:

  • MCP SDK 仅 ESM;package.json 需 "type": "module"
  • stdio 传输:日志必须写 stderr,禁止污染 stdout(会破坏 JSON-RPC)
  • 开发期可用 npx @modelcontextprotocol/inspector 调试工具列表

Cursor / Claude Code 注册 MCP 见 .cursor/mcp.json 或 ~/.cursor/mcp.json(第九章)。

第 10 步:本课程常见 npm 包速查 ​

包章节用途
openai2.xChat、Embedding
dotenv2.x本地 .env
nextLab 1App Router、API Route
@langchain/core 等6.xLangChain.js
@modelcontextprotocol/sdk3.xMCP Server
zod2.6 / 3.x结构化输出、工具参数校验
ai(Vercel AI SDK)6.7React 流式 UI 封装

动手练习 ​

  1. npm init + ESM,安装 openai、dotenv,写 chat.mjs 完成一次非流式对话。
  2. 实现 POST 本地脚本模拟 Lab:readline 读用户输入,维护 messages[] 多轮历史。
  3. 为 chat 增加 stream: true,终端逐字输出;加 AbortController,5 秒后自动 abort 试一次。
  4. 写 createClient(process.env.LLM_PROVIDER),切换 openai 与 ollama,业务代码不变。
  5. (可选)用 TypeScript 写一个最小 MCP tool,返回 { content: [{ type: 'text', text: 'pong' }] }。

常见问题 ​

Q:import 报 ERR_REQUIRE_ESM?

在 package.json 加 "type": "module",或把文件改为 .mjs。

Q:Next.js 里 fetch 报证书或代理问题?

企业网络需配置 HTTP_PROXY;本地 Ollama 用 http://127.0.0.1:11434/v1。

Q:和 Python 后端怎么拆?

Next.js 只做 BFF(Backend for Frontend):鉴权、SSE、聚合;RAG 检索调 Python FastAPI POST /internal/rag/query。Key 只存在于服务端环境变量。

Q:npm 还是 pnpm?

个人练习 npm 即可;本仓库 ai-news 站点用 npm。团队 monorepo 可统一 pnpm。

本节小结 ​

跑通本课程 Node 侧需要:Node 20+、ESM、dotenv、OpenAI SDK 或 fetch、async/await、Next.js Route Handler、流式 ReadableStream 与基础错误重试。MCP 与 Agent IDE 章节在此基础上扩展 TypeScript 与 stdio 约束——边做 Lab 边查即可。