{"product_id":"retrieval-agent-xay-dựng-agentic-rag-với-claude","title":"Retrieval Agent — Xây dựng Agentic RAG với Claude","description":"\n\u003cp\u003eRAG (Retrieval-Augmented Generation) đã trở thành kỹ thuật tiêu chuẩn để cung cấp kiến thức cho AI model. Tuy nhiên, RAG truyền thống có nhiều hạn chế: query cố định, một lần truy xuất duy nhất, không đánh giá chất lượng kết quả. Agentic RAG giải quyết tất cả vấn đề này bằng cách biến quá trình retrieval thành một vòng lặp agent — nơi Claude tự quyết định cần tìm gì, từ đâu, và liệu kết quả đã đủ tốt chưa.\u003c\/p\u003e\n\n\u003ch2\u003eBasic RAG vs Agentic RAG\u003c\/h2\u003e\n\n\u003ch3\u003eBasic RAG — Quy trình tuyến tính\u003c\/h3\u003e\n\u003cp\u003eTrong RAG truyền thống, quy trình diễn ra theo một chiều cố định:\u003c\/p\u003e\n\u003col\u003e\n  \u003cli\u003eNgười dùng đặt câu hỏi\u003c\/li\u003e\n  \u003cli\u003eHệ thống embed câu hỏi thành vector\u003c\/li\u003e\n  \u003cli\u003eTìm kiếm top-K documents gần nhất trong vector database\u003c\/li\u003e\n  \u003cli\u003eĐưa documents vào context cho LLM\u003c\/li\u003e\n  \u003cli\u003eLLM sinh câu trả lời dựa trên context\u003c\/li\u003e\n\u003c\/ol\u003e\n\u003cp\u003eHạn chế của cách tiếp cận này:\u003c\/p\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eMột lần truy xuất:\u003c\/strong\u003e Nếu kết quả lần đầu không tốt, không có cơ hội thử lại\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eQuery gốc có thể không tối ưu:\u003c\/strong\u003e Câu hỏi của người dùng thường mơ hồ hoặc thiếu context\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eKhông đánh giá chất lượng:\u003c\/strong\u003e Hệ thống không biết documents trả về có thực sự liên quan không\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eMột nguồn duy nhất:\u003c\/strong\u003e Chỉ tìm trong một vector store, không thể kết hợp nhiều nguồn\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch3\u003eAgentic RAG — Agent quyết định retrieval\u003c\/h3\u003e\n\u003cp\u003eAgentic RAG đặt một agent (Claude) vào vị trí điều phối. Agent có quyền:\u003c\/p\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eQuyết định có cần retrieval không:\u003c\/strong\u003e Một số câu hỏi Claude đã biết, không cần tìm kiếm\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eReformulate query:\u003c\/strong\u003e Chuyển câu hỏi mơ hồ thành query chính xác hơn\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eChọn nguồn tìm kiếm:\u003c\/strong\u003e Vector DB, SQL database, API, web search\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eĐánh giá kết quả:\u003c\/strong\u003e Xem documents có liên quan không, nếu không thì tìm lại\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eMulti-step retrieval:\u003c\/strong\u003e Tìm kiếm nhiều lần với query khác nhau để bổ sung thông tin\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eSynthesize:\u003c\/strong\u003e Tổng hợp từ nhiều nguồn thành câu trả lời chính xác\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch2\u003eKiến trúc Agentic RAG\u003c\/h2\u003e\n\u003cp\u003eKiến trúc Agentic RAG bao gồm ba lớp chính:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ Kien truc tong quan\n\n\/\/ Lop 1: Agent Core (Claude)\n\/\/ - Nhan cau hoi tu nguoi dung\n\/\/ - Quyet dinh chien luoc retrieval\n\/\/ - Danh gia ket qua\n\/\/ - Sinh cau tra loi cuoi cung\n\n\/\/ Lop 2: Retrieval Tools\n\/\/ - Vector search (semantic similarity)\n\/\/ - Keyword search (BM25, full-text)\n\/\/ - SQL query (structured data)\n\/\/ - Web search (real-time information)\n\n\/\/ Lop 3: Knowledge Sources\n\/\/ - Vector DB (Pinecone, Chroma, Qdrant)\n\/\/ - Relational DB (PostgreSQL)\n\/\/ - Document store (S3, file system)\n\/\/ - External APIs\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eTriển khai với Claude SDK\u003c\/h2\u003e\n\u003cp\u003eDưới đây là triển khai hoàn chỉnh một Retrieval Agent sử dụng Claude SDK với TypeScript.\u003c\/p\u003e\n\n\u003ch3\u003eBước 1: Định nghĩa retrieval tools\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/tools.ts\nimport Anthropic from \"@anthropic-ai\/sdk\";\n\nexport const retrievalTools: Anthropic.Messages.Tool[] = [\n  {\n    name: \"vector_search\",\n    description: \"Tim kiem tai lieu theo ngu nghia (semantic search). \"\n      + \"Dung khi can tim tai lieu lien quan den mot chu de \"\n      + \"hoac khai niem. Tra ve cac doan van ban gan nghia nhat \"\n      + \"voi query.\",\n    input_schema: {\n      type: \"object\",\n      properties: {\n        query: {\n          type: \"string\",\n          description: \"Cau truy van tim kiem. \"\n            + \"Nen la cau mo ta chi tiet ve thong tin can tim.\"\n        },\n        collection: {\n          type: \"string\",\n          enum: [\"product_docs\", \"support_tickets\",\n                 \"knowledge_base\", \"policies\"],\n          description: \"Bo suu tap tai lieu can tim.\"\n        },\n        top_k: {\n          type: \"number\",\n          description: \"So luong ket qua toi da (mac dinh 5).\"\n        },\n        min_score: {\n          type: \"number\",\n          description: \"Diem tuong dong toi thieu (0-1, mac dinh 0.7).\"\n        }\n      },\n      required: [\"query\", \"collection\"]\n    }\n  },\n  {\n    name: \"keyword_search\",\n    description: \"Tim kiem theo tu khoa chinh xac (full-text search). \"\n      + \"Dung khi can tim tai lieu chua tu khoa cu the \"\n      + \"nhu ma san pham, ten rieng, thuat ngu chuyen mon.\",\n    input_schema: {\n      type: \"object\",\n      properties: {\n        keywords: {\n          type: \"string\",\n          description: \"Tu khoa tim kiem, cach nhau boi dau cach.\"\n        },\n        collection: {\n          type: \"string\",\n          enum: [\"product_docs\", \"support_tickets\",\n                 \"knowledge_base\", \"policies\"]\n        },\n        exact_match: {\n          type: \"boolean\",\n          description: \"Tim chinh xac cum tu (mac dinh false).\"\n        }\n      },\n      required: [\"keywords\", \"collection\"]\n    }\n  },\n  {\n    name: \"sql_query\",\n    description: \"Truy van du lieu co cau truc tu database. \"\n      + \"Dung khi can thong tin so lieu, thong ke, \"\n      + \"hoac du lieu bang bieu.\",\n    input_schema: {\n      type: \"object\",\n      properties: {\n        question: {\n          type: \"string\",\n          description: \"Cau hoi bang ngon ngu tu nhien \"\n            + \"ve du lieu can truy van.\"\n        },\n        tables: {\n          type: \"array\",\n          items: { type: \"string\" },\n          description: \"Danh sach bang lien quan.\"\n        }\n      },\n      required: [\"question\"]\n    }\n  },\n  {\n    name: \"evaluate_sources\",\n    description: \"Danh gia chat luong va do tin cay cua cac \"\n      + \"nguon thong tin da thu thap. Goi sau khi da co \"\n      + \"ket qua tu cac tool tim kiem khac.\",\n    input_schema: {\n      type: \"object\",\n      properties: {\n        sources: {\n          type: \"array\",\n          items: {\n            type: \"object\",\n            properties: {\n              content: { type: \"string\" },\n              source_id: { type: \"string\" },\n              relevance_score: { type: \"number\" }\n            }\n          },\n          description: \"Danh sach nguon thong tin can danh gia.\"\n        },\n        original_question: {\n          type: \"string\",\n          description: \"Cau hoi goc cua nguoi dung.\"\n        }\n      },\n      required: [\"sources\", \"original_question\"]\n    }\n  }\n];\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eBước 2: Implement tool handlers\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/handlers.ts\nimport { ChromaClient } from \"chromadb\";\nimport { Pool } from \"pg\";\n\nconst chroma = new ChromaClient({ path: \"http:\/\/localhost:8000\" });\nconst pgPool = new Pool({\n  connectionString: process.env.DATABASE_URL\n});\n\nexport async function handleToolCall(\n  name: string, input: any\n): Promise\u0026lt;string\u0026gt; {\n  switch (name) {\n    case \"vector_search\":\n      return await vectorSearch(input);\n    case \"keyword_search\":\n      return await keywordSearch(input);\n    case \"sql_query\":\n      return await sqlQuery(input);\n    case \"evaluate_sources\":\n      return await evaluateSources(input);\n    default:\n      throw new Error(\"Unknown tool: \" + name);\n  }\n}\n\nasync function vectorSearch(input: {\n  query: string;\n  collection: string;\n  top_k?: number;\n  min_score?: number;\n}): Promise\u0026lt;string\u0026gt; {\n  const { query, collection, top_k = 5, min_score = 0.7 } = input;\n\n  const col = await chroma.getCollection({ name: collection });\n  const results = await col.query({\n    queryTexts: [query],\n    nResults: top_k,\n  });\n\n  \/\/ Loc theo min_score\n  const filtered = [];\n  for (let i = 0; i \u0026lt; (results.ids[0]?.length || 0); i++) {\n    const distance = results.distances?.[0]?.[i] || 1;\n    const score = 1 - distance; \/\/ Chuyen distance thanh similarity\n    if (score \u0026gt;= min_score) {\n      filtered.push({\n        id: results.ids[0][i],\n        content: results.documents[0]?.[i] || \"\",\n        metadata: results.metadatas?.[0]?.[i] || {},\n        relevance_score: Math.round(score * 100) \/ 100,\n      });\n    }\n  }\n\n  return JSON.stringify({\n    total_found: filtered.length,\n    query: query,\n    results: filtered,\n  });\n}\n\nasync function keywordSearch(input: {\n  keywords: string;\n  collection: string;\n  exact_match?: boolean;\n}): Promise\u0026lt;string\u0026gt; {\n  const { keywords, collection, exact_match = false } = input;\n\n  \/\/ Su dung PostgreSQL full-text search\n  const searchQuery = exact_match\n    ? \"SELECT id, content, metadata, \"\n      + \"ts_rank(search_vector, phraseto_tsquery($1)) as rank \"\n      + \"FROM \" + collection + \" \"\n      + \"WHERE search_vector @@ phraseto_tsquery($1) \"\n      + \"ORDER BY rank DESC LIMIT 10\"\n    : \"SELECT id, content, metadata, \"\n      + \"ts_rank(search_vector, plainto_tsquery($1)) as rank \"\n      + \"FROM \" + collection + \" \"\n      + \"WHERE search_vector @@ plainto_tsquery($1) \"\n      + \"ORDER BY rank DESC LIMIT 10\";\n\n  const result = await pgPool.query(searchQuery, [keywords]);\n\n  return JSON.stringify({\n    total_found: result.rows.length,\n    keywords: keywords,\n    results: result.rows.map(row =\u0026gt; ({\n      id: row.id,\n      content: row.content,\n      metadata: row.metadata,\n      relevance_score: Math.round(row.rank * 100) \/ 100,\n    })),\n  });\n}\n\nasync function sqlQuery(input: {\n  question: string;\n  tables?: string[];\n}): Promise\u0026lt;string\u0026gt; {\n  \/\/ Trong production, su dung text-to-SQL model\n  \/\/ hoac predefined queries de dam bao an toan\n  const safeQueries: Record\u0026lt;string, string\u0026gt; = {\n    \"don hang\": \"SELECT * FROM orders ORDER BY created_at DESC LIMIT 20\",\n    \"san pham ban chay\": \"SELECT p.name, SUM(oi.quantity) as total_sold \"\n      + \"FROM order_items oi JOIN products p ON oi.product_id = p.id \"\n      + \"GROUP BY p.id, p.name ORDER BY total_sold DESC LIMIT 10\",\n    \"doanh thu\": \"SELECT DATE_TRUNC('month', created_at) as month, \"\n      + \"SUM(total_amount) as revenue \"\n      + \"FROM orders WHERE status = 'completed' \"\n      + \"GROUP BY month ORDER BY month DESC LIMIT 12\",\n  };\n\n  \/\/ Tim query phu hop nhat\n  const question = input.question.toLowerCase();\n  let matchedQuery = null;\n  for (const [key, sql] of Object.entries(safeQueries)) {\n    if (question.includes(key)) {\n      matchedQuery = sql;\n      break;\n    }\n  }\n\n  if (!matchedQuery) {\n    return JSON.stringify({\n      error: \"Khong tim thay query phu hop. \"\n        + \"Cac loai truy van ho tro: don hang, \"\n        + \"san pham ban chay, doanh thu.\",\n    });\n  }\n\n  const result = await pgPool.query(matchedQuery);\n  return JSON.stringify({\n    question: input.question,\n    row_count: result.rows.length,\n    data: result.rows,\n  });\n}\n\nasync function evaluateSources(input: {\n  sources: Array\u0026lt;{\n    content: string;\n    source_id: string;\n    relevance_score: number;\n  }\u0026gt;;\n  original_question: string;\n}): Promise\u0026lt;string\u0026gt; {\n  const evaluation = input.sources.map(source =\u0026gt; {\n    const hasContent = source.content.length \u0026gt; 50;\n    const isRelevant = source.relevance_score \u0026gt;= 0.7;\n\n    return {\n      source_id: source.source_id,\n      relevance_score: source.relevance_score,\n      has_sufficient_content: hasContent,\n      is_relevant: isRelevant,\n      recommendation: isRelevant \u0026amp;\u0026amp; hasContent\n        ? \"USE\" : isRelevant\n        ? \"SUPPLEMENT\" : \"DISCARD\",\n    };\n  });\n\n  const usableSources = evaluation.filter(\n    e =\u0026gt; e.recommendation !== \"DISCARD\"\n  );\n\n  return JSON.stringify({\n    total_evaluated: evaluation.length,\n    usable_sources: usableSources.length,\n    needs_more_search: usableSources.length \u0026lt; 2,\n    evaluation: evaluation,\n  });\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eBước 3: Agentic loop\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/agent.ts\nimport Anthropic from \"@anthropic-ai\/sdk\";\nimport { retrievalTools } from \".\/tools.js\";\nimport { handleToolCall } from \".\/handlers.js\";\n\nconst client = new Anthropic();\n\nconst SYSTEM_PROMPT = \"Ban la mot Retrieval Agent thong minh. \"\n  + \"Nhiem vu cua ban la tra loi cau hoi cua nguoi dung \"\n  + \"bang cach tim kiem va tong hop thong tin tu nhieu nguon.\\n\\n\"\n  + \"Quy trinh lam viec:\\n\"\n  + \"1. Phan tich cau hoi - xac dinh thong tin can tim\\n\"\n  + \"2. Chon chien luoc tim kiem phu hop:\\n\"\n  + \"   - vector_search cho cau hoi ve khai niem, chu de\\n\"\n  + \"   - keyword_search cho tu khoa cu the, ma san pham\\n\"\n  + \"   - sql_query cho so lieu, thong ke\\n\"\n  + \"3. Danh gia ket qua - goi evaluate_sources\\n\"\n  + \"4. Neu ket qua chua du, reformulate query va tim lai\\n\"\n  + \"5. Tong hop cau tra loi cuoi cung voi trich dan nguon\\n\\n\"\n  + \"Nguyen tac:\\n\"\n  + \"- Luon trich dan nguon thong tin\\n\"\n  + \"- Neu khong tim thay thong tin, noi ro rang\\n\"\n  + \"- Khong bao gio dua ra thong tin khong co trong nguon\\n\"\n  + \"- Uu tien do chinh xac hon do day du\";\n\nexport async function askRetrievalAgent(\n  question: string\n): Promise\u0026lt;string\u0026gt; {\n  const messages: Anthropic.Messages.MessageParam[] = [\n    { role: \"user\", content: question },\n  ];\n\n  let maxIterations = 10; \/\/ Gioi han vong lap agent\n  let iteration = 0;\n\n  while (iteration \u0026lt; maxIterations) {\n    iteration++;\n\n    const response = await client.messages.create({\n      model: \"claude-sonnet-4-20250514\",\n      max_tokens: 4096,\n      system: SYSTEM_PROMPT,\n      tools: retrievalTools,\n      messages: messages,\n    });\n\n    \/\/ Neu Claude tra loi truc tiep (khong can tool)\n    if (response.stop_reason === \"end_turn\") {\n      const textBlocks = response.content.filter(\n        b =\u0026gt; b.type === \"text\"\n      );\n      return textBlocks.map(b =\u0026gt; b.text).join(\"\n\");\n    }\n\n    \/\/ Xu ly tool calls\n    messages.push({ role: \"assistant\", content: response.content });\n\n    const toolResults: Anthropic.Messages.ToolResultBlockParam[] = [];\n\n    for (const block of response.content) {\n      if (block.type === \"tool_use\") {\n        try {\n          const result = await handleToolCall(block.name, block.input);\n          toolResults.push({\n            type: \"tool_result\",\n            tool_use_id: block.id,\n            content: result,\n          });\n        } catch (error) {\n          toolResults.push({\n            type: \"tool_result\",\n            tool_use_id: block.id,\n            content: \"Error: \" + (error as Error).message,\n            is_error: true,\n          });\n        }\n      }\n    }\n\n    messages.push({ role: \"user\", content: toolResults });\n  }\n\n  return \"Agent da vuot qua so vong lap toi da \"\n    + \"ma chua tim duoc cau tra loi.\";\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eMulti-step Retrieval\u003c\/h2\u003e\n\u003cp\u003eSức mạnh thực sự của Agentic RAG nằm ở khả năng thực hiện nhiều bước retrieval liên tiếp. Agent phân tích câu hỏi phức tạp thành nhiều sub-query và tổng hợp kết quả.\u003c\/p\u003e\n\n\u003ch3\u003eVí dụ: Câu hỏi đa chiều\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003eCau hoi: \"So sanh doanh thu Q1\/2025 voi Q1\/2024,\nva phan tich nguyen nhan tu feedback khach hang\"\n\nAgent se thuc hien:\nBuoc 1: sql_query - Lay doanh thu Q1\/2025\nBuoc 2: sql_query - Lay doanh thu Q1\/2024\nBuoc 3: vector_search - Tim feedback khach hang\n         lien quan den su thay doi doanh thu\nBuoc 4: evaluate_sources - Danh gia chat luong thong tin\nBuoc 5: (Neu can) keyword_search - Tim them chi tiet\n         ve san pham cu the duoc nhac den\nBuoc 6: Tong hop bao cao so sanh voi trich dan nguon\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eQuery Reformulation\u003c\/h2\u003e\n\u003cp\u003eKhi kết quả tìm kiếm lần đầu không đủ tốt, agent tự động reformulate query để tìm lại. Đây là một trong những lợi thế lớn nhất so với RAG truyền thống.\u003c\/p\u003e\n\u003cp\u003eCác chiến lược reformulation:\u003c\/p\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eExpansion:\u003c\/strong\u003e Mở rộng query với các từ đồng nghĩa hoặc liên quan. Ví dụ: \"lỗi thanh toán\" mở rộng thành \"payment error, giao dịch thất bại, không thanh toán được\"\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eDecomposition:\u003c\/strong\u003e Tách câu hỏi phức tạp thành nhiều câu hỏi đơn giản\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eAbstraction:\u003c\/strong\u003e Nâng câu hỏi cụ thể lên mức khái quát hơn. Ví dụ: \"iPhone 15 Pro Max pin yếu\" trở thành \"vấn đề pin điện thoại Apple\"\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eSpecification:\u003c\/strong\u003e Thu hẹp phạm vi tìm kiếm. Ví dụ: \"vấn đề giao hàng\" thu hẹp thành \"giao hàng chậm khu vực TP.HCM tháng 3\/2025\"\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch2\u003eSource Evaluation — Đánh giá nguồn tin\u003c\/h2\u003e\n\u003cp\u003eAgent đánh giá chất lượng nguồn thông tin trước khi sử dụng, dựa trên các tiêu chí:\u003c\/p\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eRelevance (Liên quan):\u003c\/strong\u003e Tài liệu có thực sự trả lời câu hỏi không? Đo bằng similarity score và keyword overlap\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eRecency (Mới):\u003c\/strong\u003e Thông tin có còn cập nhật không? Quan trọng cho dữ liệu thay đổi theo thời gian\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eAuthority (Uy tín):\u003c\/strong\u003e Nguồn thông tin có đáng tin cậy không? Tài liệu chính thức có trọng số cao hơn ghi chú nội bộ\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eCompleteness (Đầy đủ):\u003c\/strong\u003e Tài liệu có cung cấp đủ thông tin hay chỉ đề cập một phần?\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch2\u003eVector DB Integration\u003c\/h2\u003e\n\u003cp\u003eAgentic RAG hoạt động tốt nhất khi kết hợp với vector database chất lượng. Dưới đây là hướng dẫn tích hợp hai lựa chọn phổ biến.\u003c\/p\u003e\n\n\u003ch3\u003ePinecone — Managed vector database\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/vector-stores\/pinecone.ts\nimport { Pinecone } from \"@pinecone-database\/pinecone\";\n\nconst pinecone = new Pinecone({\n  apiKey: process.env.PINECONE_API_KEY!,\n});\n\nexport async function searchPinecone(\n  query: string,\n  namespace: string,\n  topK: number = 5\n) {\n  const index = pinecone.index(\"knowledge-base\");\n\n  \/\/ Embed query bang API embedding\n  const embedding = await embedText(query);\n\n  const results = await index.namespace(namespace).query({\n    vector: embedding,\n    topK: topK,\n    includeMetadata: true,\n    includeValues: false,\n  });\n\n  return results.matches?.map(match =\u0026gt; ({\n    id: match.id,\n    score: match.score || 0,\n    content: (match.metadata as any)?.content || \"\",\n    source: (match.metadata as any)?.source || \"\",\n    date: (match.metadata as any)?.date || \"\",\n  })) || [];\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eChroma — Self-hosted vector database\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/vector-stores\/chroma.ts\nimport { ChromaClient, OpenAIEmbeddingFunction } from \"chromadb\";\n\nconst chroma = new ChromaClient({ path: \"http:\/\/localhost:8000\" });\n\nconst embedder = new OpenAIEmbeddingFunction({\n  openai_api_key: process.env.OPENAI_API_KEY!,\n  openai_model: \"text-embedding-3-small\",\n});\n\nexport async function searchChroma(\n  query: string,\n  collectionName: string,\n  topK: number = 5,\n  minScore: number = 0.7\n) {\n  const collection = await chroma.getCollection({\n    name: collectionName,\n    embeddingFunction: embedder,\n  });\n\n  const results = await collection.query({\n    queryTexts: [query],\n    nResults: topK,\n    where: undefined,\n  });\n\n  const items = [];\n  for (let i = 0; i \u0026lt; (results.ids[0]?.length || 0); i++) {\n    const distance = results.distances?.[0]?.[i] || 1;\n    const score = 1 - distance;\n    if (score \u0026gt;= minScore) {\n      items.push({\n        id: results.ids[0][i],\n        score: Math.round(score * 100) \/ 100,\n        content: results.documents[0]?.[i] || \"\",\n        metadata: results.metadatas?.[0]?.[i] || {},\n      });\n    }\n  }\n\n  return items;\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eHybrid Search — Kết hợp vector và keyword\u003c\/h2\u003e\n\u003cp\u003eHybrid search kết hợp ưu điểm của cả semantic search (vector) và keyword search (BM25) để cho kết quả tốt hơn.\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/hybrid-search.ts\nexport async function hybridSearch(\n  query: string,\n  collection: string,\n  topK: number = 10,\n  alpha: number = 0.7 \/\/ Trong so cho vector search\n): Promise\u0026lt;SearchResult[]\u0026gt; {\n  \/\/ Chay dong thoi ca hai loai search\n  const [vectorResults, keywordResults] = await Promise.all([\n    searchChroma(query, collection, topK * 2),\n    keywordSearchPg(query, collection, topK * 2),\n  ]);\n\n  \/\/ Reciprocal Rank Fusion (RRF)\n  const scores = new Map\u0026lt;string, number\u0026gt;();\n  const contents = new Map\u0026lt;string, any\u0026gt;();\n  const k = 60; \/\/ RRF constant\n\n  \/\/ Tinh diem tu vector results\n  vectorResults.forEach((result, rank) =\u0026gt; {\n    const rrf = alpha * (1 \/ (k + rank + 1));\n    scores.set(result.id, (scores.get(result.id) || 0) + rrf);\n    contents.set(result.id, result);\n  });\n\n  \/\/ Tinh diem tu keyword results\n  keywordResults.forEach((result, rank) =\u0026gt; {\n    const rrf = (1 - alpha) * (1 \/ (k + rank + 1));\n    scores.set(result.id, (scores.get(result.id) || 0) + rrf);\n    if (!contents.has(result.id)) {\n      contents.set(result.id, result);\n    }\n  });\n\n  \/\/ Sap xep theo diem tong hop\n  return Array.from(scores.entries())\n    .sort((a, b) =\u0026gt; b[1] - a[1])\n    .slice(0, topK)\n    .map(([id, score]) =\u0026gt; ({\n      ...contents.get(id),\n      hybrid_score: Math.round(score * 1000) \/ 1000,\n    }));\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eEvaluation Metrics\u003c\/h2\u003e\n\u003cp\u003eĐánh giá chất lượng Agentic RAG cần đo lường trên nhiều chiều:\u003c\/p\u003e\n\n\u003ch3\u003eRetrieval Metrics\u003c\/h3\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eRecall@K:\u003c\/strong\u003e Trong K kết quả trả về, bao nhiêu phần trăm tài liệu liên quan được tìm thấy? Mục tiêu: trên 80%\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003ePrecision@K:\u003c\/strong\u003e Trong K kết quả trả về, bao nhiêu phần trăm thực sự liên quan? Mục tiêu: trên 70%\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eMRR (Mean Reciprocal Rank):\u003c\/strong\u003e Tài liệu liên quan đầu tiên xuất hiện ở vị trí nào? Mục tiêu: trên 0.8\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch3\u003eGeneration Metrics\u003c\/h3\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eFaithfulness:\u003c\/strong\u003e Câu trả lời có trung thành với nội dung nguồn không? Không bịa đặt thông tin. Mục tiêu: trên 95%\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eAnswer Relevancy:\u003c\/strong\u003e Câu trả lời có đúng với câu hỏi không? Mục tiêu: trên 85%\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eCompleteness:\u003c\/strong\u003e Câu trả lời có đầy đủ không? Có bỏ sót thông tin quan trọng không?\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch3\u003eAgent Metrics\u003c\/h3\u003e\n\u003cul\u003e\n  \u003cli\u003e\n\u003cstrong\u003eSteps to Answer:\u003c\/strong\u003e Agent cần bao nhiêu bước để trả lời? Ít bước hơn = hiệu quả hơn = chi phí thấp hơn\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eQuery Reformulation Rate:\u003c\/strong\u003e Bao nhiêu phần trăm queries cần reformulate? Tỷ lệ cao có thể chỉ ra vấn đề với embedding hoặc chunking\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eSource Coverage:\u003c\/strong\u003e Agent có sử dụng đúng nguồn dữ liệu không?\u003c\/li\u003e\n  \u003cli\u003e\n\u003cstrong\u003eCost per Query:\u003c\/strong\u003e Chi phí trung bình cho mỗi câu hỏi (API tokens + vector DB operations)\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cpre\u003e\u003ccode\u003e\/\/ src\/evaluation.ts\nexport interface EvalResult {\n  question: string;\n  expected_answer: string;\n  actual_answer: string;\n  sources_used: string[];\n  steps_taken: number;\n  total_tokens: number;\n  latency_ms: number;\n  faithfulness: number;  \/\/ 0-1\n  relevancy: number;     \/\/ 0-1\n}\n\nexport function calculateMetrics(results: EvalResult[]) {\n  const avgSteps = results.reduce(\n    (sum, r) =\u0026gt; sum + r.steps_taken, 0\n  ) \/ results.length;\n\n  const avgTokens = results.reduce(\n    (sum, r) =\u0026gt; sum + r.total_tokens, 0\n  ) \/ results.length;\n\n  const avgLatency = results.reduce(\n    (sum, r) =\u0026gt; sum + r.latency_ms, 0\n  ) \/ results.length;\n\n  const avgFaithfulness = results.reduce(\n    (sum, r) =\u0026gt; sum + r.faithfulness, 0\n  ) \/ results.length;\n\n  const avgRelevancy = results.reduce(\n    (sum, r) =\u0026gt; sum + r.relevancy, 0\n  ) \/ results.length;\n\n  return {\n    total_questions: results.length,\n    avg_steps: Math.round(avgSteps * 10) \/ 10,\n    avg_tokens: Math.round(avgTokens),\n    avg_latency_ms: Math.round(avgLatency),\n    avg_faithfulness: Math.round(avgFaithfulness * 100) \/ 100,\n    avg_relevancy: Math.round(avgRelevancy * 100) \/ 100,\n    cost_per_query_usd:\n      Math.round(avgTokens * 0.000003 * 10000) \/ 10000,\n  };\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eBước tiếp theo\u003c\/h2\u003e\n\u003cp\u003eAgentic RAG là bước tiến lớn so với RAG truyền thống, biến quá trình truy xuất thông tin từ quy trình tuyến tính thành vòng lặp thông minh. Bắt đầu với kiến trúc đơn giản (một vector store, vài retrieval tools), đo lường metrics, sau đó mở rộng dần. Khám phá thêm về xây dựng AI Agent tại \u003ca href=\"\/collections\/nang-cao\"\u003eThư viện Nâng cao Claude\u003c\/a\u003e.\u003c\/p\u003e\n","brand":"Minh Tuấn","offers":[{"title":"Default Title","offer_id":47730150080724,"sku":null,"price":0.0,"currency_code":"VND","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0821\/0264\/9044\/files\/retrieval-agent-xay-d_ng-agentic-rag-v_i-claude.jpg?v=1774715476","url":"https:\/\/claude.vn\/products\/retrieval-agent-xay-d%e1%bb%b1ng-agentic-rag-v%e1%bb%9bi-claude","provider":"CLAUDE.VN","version":"1.0","type":"link"}