Một mô hình ngôn ngữ có thể đưa ra câu trả lời xuất sắc ở lần chạy này, rồi trả về nội dung thiếu an toàn, sai cấu trúc hoặc có vẻ hợp lý nhưng không được chứng minh ở lần kế tiếp. Sự biến thiên đó không phải lỗi tạm thời sẽ tự biến mất khi ta đổi sang mô hình lớn hơn. Nó đến từ cách giải mã xác suất, đầu vào mơ hồ, ngữ cảnh luôn thay đổi, phiên bản mô hình mới và những công cụ bên ngoài có thể hỏng độc lập. Vì vậy, bước đầu tiên của LLM engineering đáng tin cậy là không coi một model call như hàm tất định thông thường.

Đơn vị thiết kế hữu ích là toàn bộ hệ thống bao quanh mô hình: contract trước và sau khi sinh nội dung, nguồn bằng chứng, quyền hạn, chính sách retry, eval, observability và đường chuyển giao rõ ràng cho con người. Mô hình vẫn là thành phần mạnh mẽ, nhưng nó chỉ giữ một vai trò hữu hạn trong pipeline có hành vi đo lường và kiểm soát được.

Bài viết này xây dựng pipeline cho trợ lý chăm sóc khách hàng có thể trả lời câu hỏi, tra cứu đơn hàng hoặc đề xuất hoàn tiền. Thiết kế tương tự cũng áp dụng cho trích xuất tài liệu, coding agent, tìm kiếm nội bộ và tự động hóa quy trình. Chi tiết nghiệp vụ sẽ khác, nhưng nguyên tắc trung tâm không đổi: bất định phải trở thành dữ liệu tường minh, thay vì ngoại lệ chỉ được khách hàng phát hiện sau cùng.

Bắt đầu từ tính bất định và taxonomy lỗi

Phần mềm truyền thống cũng gặp lỗi, nhưng cùng một đầu vào thường đi qua cùng các nhánh. Một request tới LLM có nhiều biến số hơn: cách dựng prompt, tài liệu được retrieve, giới hạn token, sampling, policy của nhà cung cấp, phiên bản model và kết quả công cụ. Ngay cả khi temperature bằng không, nhà cung cấp cũng không cam kết kết quả giống nhau từng bit. Vì thế, mục tiêu kiểu “99,9% request trả HTTP 200” gần như không nói được câu trả lời có hữu ích hay an toàn hay không.

Hãy định nghĩa lỗi dựa trên tác động tới người dùng và doanh nghiệp. Một taxonomy thực tế cần tách ít nhất các nhóm sau:

Nhóm lỗi Ví dụ Cách phát hiện Phản ứng ưu tiên
Giao vận Timeout, rate limit, provider trả 5xx Status code và deadline Retry có giới hạn hoặc fallback
Cú pháp JSON không hợp lệ, thiếu field bắt buộc Schema validation Repair một lần rồi fail closed
Ngữ nghĩa Đúng cấu trúc nhưng sai intent hoặc số tiền Rule, eval, consistency check Từ chối, sinh lại hoặc review
Grounding Phát biểu không có nguồn hỗ trợ Đối chiếu citation với evidence Abstain hoặc retrieve lại
Policy Tư vấn bị cấm hoặc lộ dữ liệu nhạy cảm Guardrail đầu vào/đầu ra Chặn và ghi log an toàn
Công cụ Sai tham số, side effect bị lặp Schema, authorization, idempotency Từ chối chạy hoặc dùng lại kết quả
Trải nghiệm Đúng nhưng quá chậm hoặc quá đắt Ngân sách latency và cost Route, rút gọn hoặc degrade

Taxonomy này ngăn phản xạ phổ biến nhưng nguy hiểm là “retry mọi thứ”. Timeout trước khi có side effect là ứng viên hợp lý để retry. Công cụ hoàn tiền có thể đã chạy thành công thì không, trừ khi công cụ nhận idempotency key. Policy violation tuyệt đối không nên được thử lại với cách diễn đạt hơi khác, như thể mục tiêu là lách qua bộ kiểm soát.

Reliability cũng cần service-level indicator cụ thể: task thành công, claim có grounding, từ chối đúng, output hợp lệ, tool chính xác, human escalation và latency. Số liệu tổng hợp có thể che regression ở ngôn ngữ ít dùng hoặc workflow giá trị cao, nên cần phân đoạn theo intent, model, locale, tenant và risk tier.

Warning

Không dùng chính mô hình để phán quyết hành động đặc quyền mà nó đề xuất có được phép hay không. Authorization phải nằm trong application code tất định, dựa trên người dùng đã xác thực và tham số công cụ đã validate.

Bao mô hình bằng contract, grounding và guardrail

Một request đáng tin cậy đi qua các giai đoạn có thể kiểm thử độc lập. Mô hình không bao giờ nhận credential không giới hạn và cũng không ghi trực tiếp vào system of record.

flowchart LR U[Yêu cầu người dùng] --> I[Policy đầu vào và intent] I --> R[Retrieve bằng chứng tin cậy] R --> P[Prompt và model router] P --> V[Validate schema và claim] V -->|câu trả lời| O[Guardrail đầu ra] V -->|đề xuất tool| G[Authorization gate] G --> T[Tool executor idempotent] T --> P O --> H{Rủi ro hoặc bất định cao?} H -->|không| U2[Phản hồi người dùng] H -->|có| Q[Hàng đợi human review] P -. trace, token, phiên bản .-> M[Observability và eval store] T -. kết quả .-> M Q -. quyết định .-> M

Input gate phát hiện dấu hiệu prompt injection, loại file không hỗ trợ, secret và yêu cầu nằm ngoài mục đích sản phẩm. Không nên chỉ dựa vào keyword; hãy kết hợp giới hạn tất định, classifier chuyên biệt khi thích hợp và policy của từng tenant. Nội dung bị chặn không nên đi vào log, hoặc phải được redact trước khi lưu.

Grounding cung cấp một evidence set đóng. Retrieval phải lọc quyền truy cập và độ mới, retrieve ứng viên, rerank rồi gắn source ID ổn định. Instruction cần phân biệt dữ liệu với mệnh lệnh vì tài liệu có thể chứa văn bản đối kháng. Output validator kiểm tra mọi claim quan trọng đều có nguồn thực sự hỗ trợ trong evidence set.

Structured output biến completion mơ hồ thành boundary mà phần mềm kiểm tra được. Hãy dùng JSON Schema hoặc thư viện runtime type, từ chối field lạ, giới hạn enum và miền số, đồng thời biểu diễn việc abstain một cách tường minh. Đừng parse văn xuôi bằng regular expression. Kết quả { "kind": "needs_human", "reason": "conflicting refund policies" } là một outcome được kiểm soát thành công, không phải model failure.

Guardrail nên có nhiều lớp. Rule tất định xử lý permission, giới hạn tiền, dữ liệu định danh, source membership và tool bị cấm. Classifier thống kê bao phủ nhóm an toàn tinh tế hơn, nhưng cũng cần threshold, eval set và monitoring. Không một “safety model” đơn lẻ nào là boundary hoàn chỉnh.

Hiện thực pipeline có kiểu và retry an toàn

Ví dụ TypeScript sau dùng Zod để validate model output và tool. Provider được trừu tượng hóa có chủ ý: reliability policy phải thuộc quyền sở hữu của ứng dụng, để việc đổi provider không làm business control bị viết lại.

ts
import { createHash, randomUUID } from 'node:crypto';
import { z } from 'zod';

const Answer = z.object({
  kind: z.literal('answer'),
  text: z.string().min(1).max(4_000),
  citations: z.array(z.string()).min(1),
  confidence: z.number().min(0).max(1),
}).strict();

const ToolProposal = z.object({
  kind: z.literal('tool'),
  name: z.enum(['lookup_order', 'propose_refund']),
  arguments: z.record(z.string(), z.unknown()),
  rationale: z.string().max(500),
}).strict();

const NeedsHuman = z.object({
  kind: z.literal('needs_human'),
  reason: z.string().min(1).max(500),
}).strict();

const ModelDecision = z.discriminatedUnion('kind', [
  Answer,
  ToolProposal,
  NeedsHuman,
]);

type Decision = z.infer<typeof ModelDecision>;
type Evidence = { id: string; text: string; updatedAt: string };

interface ModelClient {
  generate(input: {
    requestId: string;
    prompt: string;
    responseSchema: object;
    timeoutMs: number;
  }): Promise<unknown>;
}

class TransientModelError extends Error {}

async function withBackoff<T>(operation: () => Promise<T>): Promise<T> {
  const delays = [0, 250, 800];
  let lastError: unknown;

  for (const delay of delays) {
    if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      if (!(error instanceof TransientModelError)) throw error;
    }
  }
  throw lastError;
}

function verifyCitations(decision: Decision, evidence: Evidence[]): void {
  if (decision.kind !== 'answer') return;
  const allowed = new Set(evidence.map((item) => item.id));
  if (decision.citations.some((id) => !allowed.has(id))) {
    throw new Error('Answer contains an unknown citation');
  }
}

function authorizeTool(
  actor: { id: string; roles: string[] },
  decision: z.infer<typeof ToolProposal>,
): void {
  if (decision.name === 'propose_refund' && !actor.roles.includes('refund-agent')) {
    throw new Error('Actor cannot propose refunds');
  }
  if (decision.name === 'propose_refund') {
    const args = z.object({ orderId: z.string(), amount: z.number().positive().max(200) })
      .strict()
      .parse(decision.arguments);
    decision.arguments = args;
  }
}

async function handleRequest(input: {
  actor: { id: string; roles: string[] };
  question: string;
  evidence: Evidence[];
  primary: ModelClient;
  fallback: ModelClient;
}): Promise<Decision> {
  const requestId = randomUUID();
  const prompt = buildPrompt(input.question, input.evidence);
  const invoke = (client: ModelClient) => withBackoff(async () => {
    const raw = await client.generate({
      requestId,
      prompt,
      responseSchema: ModelDecision.toJSONSchema(),
      timeoutMs: 8_000,
    });
    return ModelDecision.parse(raw);
  });

  let decision: Decision;
  try {
    decision = await invoke(input.primary);
  } catch (error) {
    if (!(error instanceof TransientModelError)) throw error;
    decision = await invoke(input.fallback);
  }

  verifyCitations(decision, input.evidence);
  if (decision.kind === 'tool') {
    authorizeTool(input.actor, decision);
    const key = createHash('sha256')
      .update(`${requestId}:${decision.name}:${JSON.stringify(decision.arguments)}`)
      .digest('hex');
    await executeToolOnce(decision.name, decision.arguments, key);
  }
  return decision;
}

withBackoff chỉ retry lỗi đã được phân loại là transient. Code production nên thêm jitter ngẫu nhiên và tuân thủ deadline tổng của request, thay vì cấp timeout mới trọn vẹn cho mỗi attempt. Retry budget ngăn provider đang suy giảm làm lưu lượng bị nhân lên. Circuit breaker dừng gọi khi tỷ lệ lỗi gần đây vượt ngưỡng, còn bulkhead ngăn một tenant hay workflow tốn kém chiếm toàn bộ concurrency.

Fallback cần độc lập theo cách có ý nghĩa: model khác, provider khác, region khác hoặc phản hồi retrieval-only tất định. Fallback nhỏ hơn có thể nhanh và rẻ hơn, nhưng vẫn phải tuân theo cùng output contract. Với yêu cầu thông tin ít rủi ro, cached response là cách degrade rất tốt. Với chuyển tiền hay thay đổi tài khoản, fallback an toàn thường là needs_human.

Tool execution có transaction boundary riêng. Hãy lưu idempotency key và kết quả một cách atomic, rồi trả lại kết quả đã lưu cho request trùng. Approval phải gắn với đúng tool name và canonical arguments; thay đổi số tiền sau khi duyệt phải khiến approval mất hiệu lực. Capability sống ngắn an toàn hơn nhiều so với đặt credential quyền rộng vào model context.

Tip

Retry chỉ cải thiện availability khi thao tác có thể lặp an toàn, lỗi nhiều khả năng là tạm thời và deadline còn đủ cho attempt tiếp theo. Trong trường hợp khác, retry chỉ tăng cost và tail latency, đồng thời che khuất lỗi ban đầu.

Tích hợp eval và human review vào delivery

Unit test bao phủ prompt assembly, schema validation, authorization, citation membership và idempotency. Chúng không thể chứng minh chất lượng câu trả lời. Eval lấp khoảng trống đó bằng những task đại diện và cách chấm điểm tường minh. Hãy bắt đầu từ ví dụ giống production: yêu cầu phổ biến, ranh giới policy, đầu vào đa ngôn ngữ, prompt injection, retrieval rỗng, nguồn mâu thuẫn, context dài và tình huống hiếm nhưng tác động cao.

Duy trì dataset có version chứa input, evidence được phép, hành vi mong đợi, risk tier và rubric. Một số phép kiểm tra hoàn toàn tất định: schema hợp lệ, từ chối đúng lúc, citation được phép, tool và tham số chính xác. Câu trả lời tinh tế có thể cần chuyên gia gán nhãn hoặc LLM judge, nhưng judge phải được hiệu chỉnh với đánh giá của con người và đo mức bất đồng định kỳ. Không để một điểm số mờ đục duy nhất quyết định deployment.

py
from dataclasses import dataclass
from statistics import mean
from typing import Literal

@dataclass(frozen=True)
class EvalCase:
    case_id: str
    prompt: str
    expected: Literal["answer", "tool", "needs_human"]
    allowed_sources: frozenset[str]
    risk: Literal["low", "high"]

@dataclass(frozen=True)
class EvalResult:
    case_id: str
    correct_route: bool
    grounded: bool
    latency_ms: int
    cost_usd: float

def release_gate(results: list[EvalResult]) -> None:
    route_rate = mean(r.correct_route for r in results)
    grounded_rate = mean(r.grounded for r in results)
    p95_latency = sorted(r.latency_ms for r in results)[int(len(results) * 0.95) - 1]
    average_cost = mean(r.cost_usd for r in results)

    failures = []
    if route_rate < 0.98:
        failures.append(f"route accuracy {route_rate:.2%} < 98%")
    if grounded_rate < 0.995:
        failures.append(f"grounded rate {grounded_rate:.2%} < 99.5%")
    if p95_latency > 5_000:
        failures.append(f"p95 latency {p95_latency}ms > 5000ms")
    if average_cost > 0.025:
        failures.append(f"average cost ${average_cost:.4f} > $0.025")
    if failures:
        raise RuntimeError("release blocked: " + "; ".join(failures))

Chạy offline eval cho mọi thay đổi về prompt, model, retriever hoặc policy. Hãy so với cấu hình production hiện tại thay vì một mục tiêu tuyệt đối tùy ý. Duy trì các slice để cải thiện tổng thể không thể che regression ở request tiếng Việt hay xử lý hoàn tiền. Đưa failure từ incident và human review trở lại eval set, đồng thời giữ một holdout ẩn để tránh tối ưu trực tiếp theo test case.

Human review là control plane, không phải lời xin lỗi. Route request khi policy yêu cầu approval, confidence thấp, evidence mâu thuẫn, một hành động giá trị cao được đề xuất hoặc các automated check bất đồng. Reviewer cần thấy yêu cầu gốc, evidence đã retrieve, quyết định của model, kết quả validation và hành động rõ ràng, trong khi dữ liệu nhạy cảm được giảm thiểu. Override của họ trở thành ví dụ có nhãn, nhưng không nên học mù quáng từ các quyết định vội vàng hoặc thiếu nhất quán.

Quan sát, quản lý ngân sách và rollout toàn hệ thống

Một LLM trace cần nối classification, retrieval source, phiên bản prompt/model/provider, token, validation, retry, tool, route cuối, latency và cost. Với prompt nhạy cảm, chỉ lưu hash hoặc dữ liệu đã redact. Log phải trả lời được “điều gì đã thay đổi?” mà không trở thành nguồn rò rỉ mới.

Đo latency của từng stage thay vì chỉ thời gian end-to-end. Retrieval, queueing, time to first token, generation, guardrail và tool có owner lẫn cách khắc phục khác nhau. Theo dõi percentile vì average che giấu người dùng mắc kẹt sau nhiều lần retry. Cost nên được quy về mỗi task thành công, không chỉ mỗi token: model rẻ nhưng gây nhiều model call và human escalation có thể là lựa chọn đắt tiền.

Đặt budget trước khi tối ưu. Route classification đơn giản sang model nhỏ, giới hạn retrieved context, cache evidence ổn định và deterministic response an toàn, chỉ stream sau những check tương thích với streaming, đồng thời chạy song song các retrieval operation độc lập. Đừng nén prompt cho tới khi eval chứng minh chất lượng vẫn ổn định. Latency và quality là quyết định theo danh mục: hành động rủi ro cao có thể chấp nhận validation chậm hơn, còn autocomplete thì không.

Deploy thay đổi qua shadow traffic và canary. Shadow gửi một phần input production cho candidate nhưng không sử dụng output của nó; canary cho một nhóm nhỏ có tính đại diện tiếp xúc thật và theo dõi cả quality lẫn operational indicator. Pin model version khi có thể. Ghi nhận prompt, retriever, policy và model như một cấu hình deploy duy nhất để rollback khôi phục đúng tổ hợp đã biết.

Rollout gate có thể yêu cầu không có policy failure nghiêm trọng, task success không kém baseline trong confidence interval, p95 latency nằm trong giới hạn và cost dưới budget. Tăng traffic từng bước, đồng thời giữ kill switch có thể tắt riêng tool mà không tắt câu trả lời. Việc provider rollout thành công model mới không chứng minh prompt, evidence và tập người dùng của sản phẩm ta an toàn.

Ứng phó sự cố và những điều cần nhớ

Chuẩn bị runbook trước incident đầu tiên. Alert cần ánh xạ tới tác động người dùng: tỷ lệ schema hợp lệ sụt mạnh, unsupported claim tăng, tool denial tăng đột biến, retrieval freshness trễ, provider bão hòa hoặc cost mỗi task nhảy vọt. Phản ứng đầu tiên là containment: tắt tool bị ảnh hưởng, chuyển sang fallback, ép câu trả lời về retrieval-only, thu hẹp traffic hoặc bắt buộc human approval. Giữ trace ID và phiên bản cấu hình, nhưng không sao chép prompt nhạy cảm vào kênh chat xử lý sự cố.

Tiếp theo, xác định nhóm lỗi và blast radius. Truy vấn theo model version, prompt version, intent, locale, tenant, source corpus và tool. Replay một mẫu đã làm sạch trên cấu hình trước đó. Nếu model bên ngoài thay đổi âm thầm, trace đã lưu và eval baseline sẽ làm thay đổi đó lộ rõ. Khi giao tiếp về incident, hãy nói outcome nào có thể sai, không chỉ nói latency đã tăng.

Recovery không dừng ở việc sửa prompt. Thêm case của incident vào regression eval, bổ sung deterministic control còn thiếu, xem monitoring có phát hiện đủ sớm không và kiểm thử đường rollback. Câu chữ prompt có thể giảm tần suất lỗi, nhưng authorization, idempotency, schema validation và source check phải giữ invariant ngay cả khi generation mang tính đối kháng.

Những điều bền vững cần ghi nhớ:

  • Coi model output là input không đáng tin và validate bằng schema nghiêm ngặt.
  • Ground mọi claim quan trọng trong evidence set được cấp quyền và có thể kiểm tra.
  • Chặn mọi tool bằng authorization tất định và cơ chế thực thi idempotent.
  • Chỉ retry lỗi transient đã phân loại trong deadline tổng; ở nơi khác nên degrade an toàn.
  • Đánh giá hành vi theo risk và từng slice, dùng human review cho trường hợp mơ hồ hoặc tác động cao.
  • Quan sát phiên bản, chất lượng, latency và cost trên toàn bộ pipeline.
  • Rollout cấu hình model-system từng bước, đồng thời diễn tập containment và rollback.

Ứng dụng LLM đáng tin cậy không trở thành hệ thống tất định, nhưng có thể vận hành với kỷ luật. Mục tiêu không phải loại bỏ mọi câu trả lời bất định. Mục tiêu là phát hiện bất định, giới hạn phạm vi tác động của nó và trao cho hệ thống một con đường đã được kiểm thử để abstain, degrade hoặc hỏi con người trước khi bất định biến thành thiệt hại.