Skip to content
On this page

组装一个 Agent · 把零件拼成能上线的系统 ​

前面几节我们一件件点亮了能力:对话、RAG 知识、工具调用、多轮记忆。这一节把它们真正组装起来,成为一个能落地的完整 Agent。

先看清单——这正是实现篇地图上一步步攒下来的零件:

本节的 Agent 会是一个多角色的"工作流型" Agent——遇到提问,先判断该走哪条路(RAG 查资料 / 调工具 / 直接回答),再分工处理。这既体现了"组装",也展示了生产级 Agent 最常见的形态:由几张"路线图"(Graph)组成,而不是一个光秃秃的 model.invoke()。

1. 目录结构:一个可维护的 Agent 工程 ​

好代码从结构开始。我们把所有零件按职责拆开:

text
my-agent/
├─ package.json
├─ .env                      ← API Key 放这里(已被 .gitignore)
├─ data/
│   └─ 员工手册.txt          ← 知识库语料
├─ src/
│   ├─ index.js              ← 入口:启动交互终端
│   ├─ llm.js                ← 出口:统一创建"模型"单例
│   ├─ tools/
│   │   ├─ math.js           ← 工具:计算器
│   │   └─ weather.js        ← 工具:查天气
│   ├─ rag.js                ← RAG:建库/检索
│   ├─ memory.js             ← 记忆:thread 管理
│   └─ agent.js              ← 大脑:组装语言图(Graph)

2. 先做公共件:统一出口 llm.js ​

每个模块都要用模型,不如集中管理(换一家厂商只改一处):

js
// src/llm.js —— 单例出口
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'

/**
 * 全局唯一的模型实例。
 * 换模型 = 改 .env, 换厂商 = 改 configuration.baseURL, 代码一字不动。
 */
export function getModel() {
  return new ChatOpenAI({
    model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
    apiKey: process.env.OPENAI_API_KEY,
    configuration: { baseURL: process.env.OPENAI_API_BASE },
    temperature: 0.3,              // 客服型,压低随机性
  })
}

3. 组装:用一张"语言图"把分工说清楚 ​

这里用 LangGraph 把 Agent 定义成一张图。先想清楚路由规则:

┌──────────────────────────────┐
         │         router(路由)            │
         │  判断:这个问题该怎么答?          │
         └──────────┬───────────────────┘
        ┌───────────┼─────────────┐
        ▼           ▼              ▼
   有工具可办   有资料可查      直接回答
   (tool_node)  (rag_node)     (chat_node)
        │            │              │
        └────────────┴──────┬───────┘
                            ▼
                     回到 router(循环,直到解决)

用代码把这张图描述出来(@langchain/langgraph 的 StateGraph):

js
// src/agent.js —— 组装核心
import 'dotenv/config'
import { StateGraph, START, END, Annotation } from '@langchain/langgraph'
import { MemorySaver } from '@langchain/langgraph'
import { toolsCondition } from '@langchain/langgraph/prebuilt' // 帮助判断"是否还有工具要调"
import { ToolNode } from '@langchain/langgraph/prebuilt'
import { getModel } from './llm.js'
import { calculator } from './tools/math.js'
import { getWeather } from './tools/weather.js'
import { searchKnowledge } from './rag.js'

// —— ① 定义"状态": 所有节点共享的黑板 ——
const State = Annotation.Root({
  messages: Annotation({ reducer: (a, b) => a.concat(b) }), // 消息一路追加, 就是记忆
  needsRag: Annotation(),        // 路由标记:要不要查资料
  needsTool: Annotation(),       // 路由标记:要不要调工具
})

// —— ② 模型 ——
const tools = [calculator, getWeather]
const model = getModel().bindTools(tools)

// —— ③ 各类节点 ——

// 路由:决定走哪条路
async function router(state) {
  const last = state.messages[state.messages.length - 1]
  const text = last.content.toLowerCase()

  // 粗略规则:含"查/手册/规定/能不能"→ 可能查资料; 含"算/几倍/多少"且像数字 → 工具
  // 更准的做法是再问模型一次, 这里先用关键词演示"分工"思路
  const needRag = /手册|规定|报销|加班|补贴/.test(text)
  const needTool = /算|天气/.test(text)

  return {
    needsRag: needRag,
    needsTool: needTool,
    messages: state.messages,
  }
}

async function chatNode(state) {
  const resp = await model.invoke(state.messages)
  return { messages: [resp] }
}

async function ragNode(state) {
  const last = state.messages[state.messages.length - 1].content
  const context = await searchKnowledge(last)       // 从向量库捞资料
  const reply = await model.invoke([
    { role: 'system', content: '严格依据以下资料回答,没有就拒绝。\n' + context },
    { role: 'user', content: last },
  ])
  return { messages: [reply], needsRag: false }
}

// 工具节点: LangGraph 帮你做"执行工具→结果回填"的循环
const toolNode = new ToolNode(tools)

// —— ④ 连成图 ——
const workflow = new StateGraph(State)
  .addNode('router', router)
  .addNode('chat', chatNode)
  .addNode('rag', ragNode)
  .addNode('tools', toolNode)

workflow.addEdge(START, 'router')

// 路由后的三条分支
workflow.addConditionalEdges('router', (state) => {
  if (state.needsTool) return 'tools'
  if (state.needsRag) return 'rag'
  return 'chat'
})

// 工具执行完,带回模型再走路由
workflow.addEdge('tools', 'chat')
workflow.addEdge('chat', END)
workflow.addEdge('rag', END)

// —— ⑤ 装配记忆:线程持久化 ——
export const agent = workflow.compile({
  checkpointer: new MemorySaver(),  // 生产换成 SqliteSaver/Redis
})

这是实现篇最关键的十几行,值得逐行看懂:

部分作用
State一张共享的"黑板",所有节点读写它。reducer: a.concat(b) 让消息一路追加 = 记忆
router快递分拣员,读问题判断该走哪条路(实际产品应再问一次模型作"智能路由")
chat/rag/tools三个工种:直接聊 / 查资料答 / 真调工具
addConditionalEdges根据路由标记动态选下一站
checkpointer给了整套流程记忆

真实产品的"智能路由"通常也交给模型判断(让它输出一个意图标签),思路一样,只是把 router 的关键词规则换成一次低成本的模型调用。

4. 入口 index.js:一个能跑起来玩的终端 ​

js
// src/index.js
import 'dotenv/config'
import readline from 'node:readline/promises'
import { agent } from './agent.js'

const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const thread = { configurable: { thread_id: 'guest-001' } }

console.log('🤖 全栈 Agent 已就绪(输入 bye 退出)')
console.log('试试: 我周末去北京出差能报销多少? / 今天上海天气? / (12+28)*5? / 报销额度是多少?\n')

while (true) {
  const q = await rl.question('你: ')
  if (q === 'bye') break
  const result = await agent.invoke({ messages: [{ role: 'user', content: q }] }, thread)
  const last = result.messages[result.messages.length - 1]
  console.log('AI: ' + last.content + '\n')
}

跑起来看它的分工切换:

bash
npm run dev
# 你: 员工的周末加班费怎么算?
# AI: 周末加班按 2 倍计算(依据:员工手册)。→ 走了 RAG 分支
# 你: 今天深圳多少度?
# AI: 深圳当前 32°C。→ 走了 tools 分支(调 getWeather)
# 你: (88+12)*3 等于几?
# AI: = 300。→ 走了 tools 分支(调 calculator)
# 你: 我周末要出差,住在深圳,记得提醒我
# AI: 好的,记下了。→ 直接回答并存入记忆

看到没——同一个入口,面对不同问题自动切换到不同的"工种"。 这就是"组装"的意义:不是把能力堆在一起,而是由一张路由图把它们调度起来,各司其职。

5. 生产前,把这 5 件事补上 ​

实现篇到此,一个能跑的 Agent 全貌你已经有了。要上生产,还差这几件"工程小步",记住关键词,深入时再展开:

  1. 输入输出校验:给 rag.js/外部工具加 zod 校验,别让脏数据入库;
  2. 限流与速率:调模型/向量库都有配额,用队列/本地缓存(如模型结果缓存);
  3. 超时与重试:网络可能失败,给关键调用加 timeout + 重试指数退避;
  4. 观测日志:每个节点打印 node名 + 耗时 + messages长度 到日志平台(生产重要!);
  5. 人脸闸口:高风险动作(转账、删除、发布)设计成"Agent 只提建议,人批准才执行"。

这几条对应的正是基础篇"人机协同"和"边界"的工程落地——给力的事让 Agent 做,担责的闸门留给人的判断。


🔚 实现篇到这里,你已经拥有什么 ​

曾经现在
对着 OpenAI 文档发一个 POST /chat/completions一个有记忆、有知识、能调工具、懂分流的完整 Agent
只会写"调模型的单条代码"能画出并实现一张 Agent 工作流图(LangGraph)
每次对话都要重讲一遍背景用 thread_id 记住每个用户/会话
怕模型瞎编内部数据用 RAG 让它"照资料说话"

这套骨架是你后续"微调训练篇""部署篇"的前置。 当你想让它更懂你领域、或把它发布成 API / Serverless 服务时,回到这里——你的 Agent 已经打好地基。

实现篇的灵魂,一句话:

让 Agent 上生产,不是把能力堆在一起,而是用一张"路由图"把对话、RAG、工具、记忆调度起来,各司其职,由人把握边界。 到此,你已把前两篇的"道理"亲手变成了"能跑的系统"。


接下来进入 [模型篇深化] 或 [部署篇],把这位 Agent 真正推上线。敬请期待。

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命