跳转到内容

Recipe: Support Agent with a Knowledge Base (RAG)

此内容尚不支持你的语言。

This recipe builds a support agent that answers questions using your own documentation instead of the model’s general knowledge. It’s the single most common starting point for a Wetel integration, and it touches four pieces documented elsewhere in isolation — Knowledge Base (RAG), the LLM node, the Response node, and Agents — assembled into one working configuration.

What you’ll build: a knowledge base containing a product FAQ, an agent with a support persona, and a workflow that answers every incoming message using that FAQ as grounding material.

Every request below needs the x-huat-platform: customer header and a valid dashboard Authorization: Bearer <token> — see Getting Started and Authentication if you haven’t set those up yet.

A knowledge base is tenant-scoped and independent of any one agent — you attach it to a workflow’s llm node, not to the agent record itself. See Knowledge Base (RAG) for the full ingestion API this step is drawn from.

mutation CreateKB {
createKnowledgeBase(
input: {
name: "Product FAQ"
description: "Frequently asked questions about our product and billing"
}
) {
id
name
createdAt
}
}

Headers:

x-huat-platform: customer
Authorization: Bearer <your-dashboard-access-token>

Note the returned id — you’ll need it in Step 2 and Step 4. For this recipe, assume it came back as 88.

For a document this size (a typical FAQ page), inline ingestion is the simplest path — no upload/poll cycle needed. ingestDocument chunks the text (roughly 512 words per chunk, with overlap), embeds each chunk, and the knowledge base is queryable immediately after the call returns.

mutation IngestFAQ {
ingestDocument(
input: {
knowledgeBaseId: 88
fileName: "product-faq.txt"
text: "Q: How do I reset my password?\nA: Go to Settings > Account > Reset Password. A reset link is emailed within a few minutes.\n\nQ: What's your refund policy?\nA: Full refunds are available within 30 days of purchase. Contact support with your order number to start a refund.\n\nQ: Do you offer annual billing?\nA: Yes — annual plans are billed once per year and include a 15% discount versus monthly billing. Switch anytime from Settings > Billing."
}
)
}

ingestDocument returns the chunk count created (a plain integer), not an object — that’s expected, not a sign something went wrong.

If your real FAQ is a PDF or a larger DOCX file (up to 20 MB), use the three-step presigned-upload flow instead — requestKnowledgeDocumentUpload → your client uploads directly to the returned URL → confirmKnowledgeDocumentUpload → poll knowledgeDocuments until parseStatus reaches READY. See Knowledge Base (RAG) for that flow in full — this recipe doesn’t repeat it since inline text covers most FAQ-sized content.

Sanity-check retrieval before wiring it into a workflow

Section titled “Sanity-check retrieval before wiring it into a workflow”

Before spending time on the workflow graph, confirm the knowledge base actually surfaces the right chunks for realistic phrasing:

query TestRetrieval {
retrieveChunks(
knowledgeBaseId: 88
query: "how do I get my money back"
topK: 5
) {
chunkId
content
score
}
}

This runs the exact retrieval logic an llm node uses at conversation time. If the refund-policy chunk doesn’t show up near the top for a paraphrased question like this, the wording or chunk boundaries of your source document are worth revisiting before you build the rest of the workflow around it.

mutation CreateSupportAgent {
createAgent(
input: {
name: "Product Support Assistant"
position: "Support Agent"
personaPrompt: "You are a helpful, concise product support assistant. Answer questions using the reference material you're given. If something isn't covered by your reference material, say you don't have that information and suggest the user contact human support — never guess at policy details."
useCase: SUPPORT
voiceTier: STANDARD
}
) {
id
}
}

personaPrompt defines identity and tone — including the explicit instruction not to guess when the knowledge base doesn’t cover something, which matters once retrieval is wired in (see the fail-open behavior note in Step 5). voiceTier is required even though this is a text-only agent; the value is simply unused unless you activate voice later. Assume this returned agent id 61.

This workflow has no branching — every message goes through the same RAG-grounded llm node and back out through a response node. That’s intentional: for a first support agent, keeping the graph this simple makes it easy to confirm retrieval is actually working before adding intent classification or escalation branches on top.

Flow: start → answer from the knowledge base (llm with knowledgeBaseId: 88) → reply (response) → end_session.

mutation CreateWorkflow($input: CreateWorkflowInput!) {
createWorkflow(input: $input) {
id
name
isPublished
}
}
{
"input": {
"name": "Product Support (RAG)",
"description": "Answers product questions grounded in the Product FAQ knowledge base."
}
}

Assume this returned workflow id 42. Now populate the graph:

mutation UpdateWorkflow($input: UpdateWorkflowInput!) {
updateWorkflow(input: $input) {
id
version
}
}
{
"input": {
"id": 42,
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "answer_from_kb",
"type": "llm",
"label": "Answer From Product FAQ",
"config": {
"promptTemplate": "You are a helpful support assistant. Answer the user's question using the provided reference material where relevant: {{userMessage}}",
"knowledgeBaseId": 88,
"outputVar": "kb_answer"
},
"position": { "x": 0, "y": 100 }
},
{
"id": "respond",
"type": "response",
"label": "Respond With Answer",
"config": {
"messageTemplate": "{{kb_answer}}",
"mood": "helpful"
},
"position": { "x": 0, "y": 200 }
},
{
"id": "end",
"type": "end_session",
"label": "End",
"config": { "runEvaluation": true },
"position": { "x": 0, "y": 300 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "answer_from_kb" },
{ "id": "e2", "source": "answer_from_kb", "target": "respond" },
{ "id": "e3", "source": "respond", "target": "end" }
]
}
}

respond’s messageTemplate is a thin pass-through of kb_answer — the llm node already produced good, user-facing text, so there’s no reason to add a second paraphrasing step in between. See Workflow Best Practices for why this matters (extra hops add latency and a chance of the model subtly changing the meaning).

Step 5: Publish the workflow and attach it

Section titled “Step 5: Publish the workflow and attach it”

A workflow that’s been updated but never published executes zero nodes for every session — this is the single most common first-integration mistake, covered in Agents. Publish it, then attach it to the agent:

mutation PublishWorkflow($id: Int!) {
publishWorkflow(id: $id) {
id
isPublished
version
}
}
{ "id": 42 }
mutation AttachWorkflow {
updateAgent(id: "61", input: { workflowId: 42 }) {
id
workflowId
}
}

Confirm isPublished: true came back from publishWorkflow before testing — if the agent seems to ignore the graph entirely, that flag is the first thing to check.

A user starts a session against this agent and sends: “How do I get a refund on my order?”

  1. Execution enters at start and moves to answer_from_kb.
  2. Because knowledgeBaseId: 88 is set, the current message is embedded and matched against the FAQ’s chunks by similarity. The refund-policy chunk you ingested in Step 2 is retrieved and joined into a reference block, which is injected into the model’s context before it responds — the model sees this as grounding material, not as something the user typed.
  3. The model answers using that grounding material, and its output is written to kb_answer.
  4. respond sends kb_answer back to the user verbatim: something like “Full refunds are available within 30 days of purchase — just contact support with your order number to get started.”
  5. end_session closes the turn’s session lifecycle and, because runEvaluation: true is set, queues a post-session evaluation you can fetch later via the evaluation query (see Evaluation & Export).

If a user asks something the FAQ genuinely doesn’t cover, the personaPrompt’s explicit instruction not to guess is what keeps the agent from fabricating an answer — retrieval itself doesn’t filter anything out, it just changes what material is available to the model.

If the retrieval step itself fails at runtime (a transient vector-store error, not “no relevant chunks found”), the llm node fails open — it proceeds without grounding rather than failing the whole node, so the agent still responds rather than the turn erroring out. This is a deliberate reliability tradeoff: a knowledge base being briefly unreachable shouldn’t take your agent offline. See Knowledge Base (RAG) for how to distinguish “retrieval ran and found nothing” from “retrieval didn’t run at all” if you need to confirm which happened for a given reply.