Appearance
组装一个 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 全貌你已经有了。要上生产,还差这几件"工程小步",记住关键词,深入时再展开:
- 输入输出校验:给
rag.js/外部工具加 zod 校验,别让脏数据入库; - 限流与速率:调模型/向量库都有配额,用队列/本地缓存(如模型结果缓存);
- 超时与重试:网络可能失败,给关键调用加
timeout+ 重试指数退避; - 观测日志:每个节点打印
node名 + 耗时 + messages长度到日志平台(生产重要!); - 人脸闸口:高风险动作(转账、删除、发布)设计成"Agent 只提建议,人批准才执行"。
这几条对应的正是基础篇"人机协同"和"边界"的工程落地——给力的事让 Agent 做,担责的闸门留给人的判断。
🔚 实现篇到这里,你已经拥有什么
| 曾经 | 现在 |
|---|---|
对着 OpenAI 文档发一个 POST /chat/completions | 一个有记忆、有知识、能调工具、懂分流的完整 Agent |
| 只会写"调模型的单条代码" | 能画出并实现一张 Agent 工作流图(LangGraph) |
| 每次对话都要重讲一遍背景 | 用 thread_id 记住每个用户/会话 |
| 怕模型瞎编内部数据 | 用 RAG 让它"照资料说话" |
这套骨架是你后续"微调训练篇""部署篇"的前置。 当你想让它更懂你领域、或把它发布成 API / Serverless 服务时,回到这里——你的 Agent 已经打好地基。
实现篇的灵魂,一句话:
让 Agent 上生产,不是把能力堆在一起,而是用一张"路由图"把对话、RAG、工具、记忆调度起来,各司其职,由人把握边界。 到此,你已把前两篇的"道理"亲手变成了"能跑的系统"。
接下来进入 [模型篇深化] 或 [部署篇],把这位 Agent 真正推上线。敬请期待。