本指南将一步步教你如何把 常见问题与知识库助手 模板接入你自己的应用。无需任何 AI 经验,你只需要复制代码,而不是编写 AI 逻辑。
它做什么
基于你的知识库回答常见客户问题,并标记需要人工介入的问题。
模板自带 1 个工作流和 1 个响应格式,它们已经可以协同工作:
- 工作流:
faq_knowledge_workflow - 响应格式:
faq_answer_response
想走完全无代码的路径? 直接跳到 第 6 步 - Go no-code。
你将构建什么
- 一个后端端点,用客户请求启动 常见问题与知识库助手 工作流。
- 一个实时聊天界面,在生成过程中流式展示 AI 回复。
- 一个Webhook 接收器,接收流水线事件并把结果发回给客户。
Step 1 - Download and import the template
- 打开 常见问题与知识库助手 模板页面,点击下载 JSON。
- 在 ModelRiver 项目中点击导入,粘贴或上传文件,确认预览后完成导入。所有内容以原子方式创建,不会覆盖任何已有数据。
- 了解更多:导入与导出指南。
Step 2 - Connect the AI providers
该模板运行在 xAI 和 Anthropic 上。导入前请先在项目的「提供商」部分完成连接。如果你使用其他提供商,也可以在导入后修改任意工作流。
Step 3 - Add the backend endpoint
你的后端通过调用 ModelRiver API 来启动工作流,需要项目 API 密钥(在「项目设置 → API 密钥」中创建)。响应中包含一次性 ws_token,你的前端用它来流式接收回复。
// Express.js - POST /api/ai/chatimport express from 'express';const app = express();app.use(express.json()); app.post('/api/ai/chat', async (req, res) => { const response = await fetch('https://api.modelriver.com/v1/ai/async', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MR_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ workflow: 'faq_knowledge_workflow', messages: [{ role: 'user', content: req.body.message }], inputs: { customer_id: req.body.customer_id }, }), }); const data = await response.json(); // Return the one-time ws_token so the frontend can stream the reply. res.json({ ws_token: data.ws_token, channel_id: data.channel_id });}); app.listen(3000);也可以使用 CLI 在本地运行这个端点:modelriver listen 会把 ModelRiver 的 Webhook 转发到你的 localhost。
Step 4 - Stream the reply in your frontend
使用 ModelRiver 客户端 SDK 连接数据流。用后端返回的 ws_token 调用 connect() 后,AI 回复会实时到达,无需轮询。
import { useState } from 'react';import { useModelRiver } from '@modelriver/client/react'; export function TemplateChat() { const [input, setInput] = useState(''); const { connect, response, error, status } = useModelRiver({ baseUrl: 'wss://api.modelriver.com/socket', persist: true, }); const handleSend = async () => { // 1. Your backend starts the faq_knowledge_workflow workflow and returns a one-time ws_token. const res = await fetch('/api/ai/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: input }), }); const { ws_token, channel_id } = await res.json(); // 2. Connect to ModelRiver's stream. The reply arrives in real time. connect({ wsToken: ws_token, channelId: channel_id }); setInput(''); }; return ( <div> <input value={input} onChange={(e) => setInput(e.target.value)} disabled={status === 'loading'} placeholder="Type your request..." /> <button onClick={handleSend} disabled={!input || status === 'loading'}>Send</button> {status === 'loading' && <p>Working on it...</p>} {response && ( <div> <p>{response.content}</p> {response.data && <pre>{JSON.stringify(response.data, null, 2)}</pre>} </div> )} {error && <p>{error}</p>} </div> );}用 npm install @modelriver/client 安装 SDK。同一个 ws_token 可用于 React、Vue、原生 JS 和 Svelte。
Step 5 - Receive the pipeline event in your backend
AI 步骤完成后,ModelRiver 会把 task.ai_generated 事件投递到你项目中配置的 Webhook 地址。你的后端可以补充自己的数据(你的政策、订单详情或数据库查询),然后调用事件中的 callback_url,最终回复就会流式发送给客户。
每次投递都是一个 JSON 信封(callback_url、channel_id、data、timestamp),事件本身(type、event、ai_response、callback_url、customer_data 等)位于 data 中。请使用 x-modelriver-signature 和 x-modelriver-timestamp 请求头进行校验:签名是以你的 Webhook 密钥对 <x-modelriver-timestamp>.<data> 计算的 HMAC-SHA256(十六进制),其中 <data> 是从原始请求体中截取的 data 值的原始 JSON 文本(第一个 "data": 之后、最后一个 ,"timestamp": 之前)。切勿对解析后的 JSON 重新序列化;请使用常量时间比较,并拒绝早于约 5 分钟的时间戳。
// Express.js - POST /webhooks/modelriverimport express from 'express';import crypto from 'node:crypto'; const app = express(); // Keep the raw body: the signature covers the exact text ModelRiver sent.app.post('/webhooks/modelriver', express.raw({ type: 'application/json' }), async (req, res) => { // 1. Verify the HMAC signature: hex HMAC-SHA256 of "<timestamp>.<data>", where // <data> is the exact JSON text of the body's "data" value (never re-serialize it). const raw = req.body.toString('utf8'); const timestamp = req.get('x-modelriver-timestamp') || ''; const signature = Buffer.from(req.get('x-modelriver-signature') || ''); const start = raw.indexOf('"data":') + '"data":'.length; const end = raw.lastIndexOf(',"timestamp":'); const expected = Buffer.from(crypto .createHmac('sha256', process.env.MR_WEBHOOK_SECRET) .update(`${timestamp}.${raw.slice(start, end)}`) .digest('hex')); if ( start < 7 || end < start || signature.length !== expected.length || !crypto.timingSafeEqual(signature, expected) ) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(raw).data; // the event payload is under "data" // 2. The AI step finished. Enrich it with your own data (order, policy, ...). const enriched = { ...event.ai_response, checked_at: new Date().toISOString() }; await saveEvent(event.channel_id, enriched); // your database // 3. Send the enriched result back so the reply streams to the customer. if (event.callback_url && event.callback_required) { await fetch(event.callback_url, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MR_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: enriched }), }); } res.status(200).send('ok');}); app.listen(3000);Step 6 - Go no-code
不想自己运行服务器?这些平台可以直接接收流水线事件、存储数据并回调,无需管理任何基础设施。
// Supabase Edge Function - webhooks/modelriver// Deploy with: supabase functions deploy webhooks-modelriverimport { serve } from 'https://deno.land/[email protected]/http/server.ts';import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!); // Signed string: "<x-modelriver-timestamp>.<exact JSON text of the body's data value>".async function verifySignature(raw: string, timestamp: string | null, signature: string | null): Promise<boolean> { const start = raw.indexOf('"data":') + 7; const end = raw.lastIndexOf(',"timestamp":'); if (!timestamp || !signature || start < 7 || end < start) return false; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(Deno.env.get('MR_WEBHOOK_SECRET')!), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const digest = new Uint8Array(await crypto.subtle.sign( 'HMAC', key, new TextEncoder().encode(`${timestamp}.${raw.slice(start, end)}`) )); const expected = [...digest] .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); if (signature.length !== expected.length) return false; let diff = 0; for (let i = 0; i < expected.length; i++) diff |= signature.charCodeAt(i) ^ expected.charCodeAt(i); return diff === 0;} serve(async (req) => { // 1. Verify the HMAC signature over the raw body before parsing the event. const raw = await req.text(); const valid = await verifySignature( raw, req.headers.get('x-modelriver-timestamp'), req.headers.get('x-modelriver-signature')); if (!valid) return new Response('Invalid signature', { status: 401 }); const event = JSON.parse(raw).data; // the event payload is under "data" // 2. Store the AI step result in Postgres. const { error } = await supabase.from('ai_content').insert({ channel_id: event.channel_id, payload: event.ai_response, status: 'ai_generated', }); if (error) return new Response(error.message, { status: 500 }); // 3. Send the result back so the reply streams to the customer. if (event.callback_url && event.callback_required) { await fetch(event.callback_url, { method: 'POST', headers: { Authorization: `Bearer ${Deno.env.get('MR_API_KEY')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: event.ai_response }), }); } return new Response('ok');});Test it end to end
- 启动你的后端(或部署无代码函数),并在项目中配置
MR_API_KEY、MR_WEBHOOK_SECRET和 Webhook 地址。 - 打开前端,输入一条测试请求并点击发送。
- 确认回复实时到达,并且后端收到了
task.ai_generated事件。