原文地址:https://dev.to/torkian/add-guardrails-so-your-ai-app-doesnt-lie-a-two-layer-approach-with-nvidia-nim-3km
RAG 应用最怕的,不是模型不会答,而是它明明没有证据,答出来却又特别像真的。
更麻烦的是,很多 Demo 看着已经“会检索、会回答”了,但一到真实使用时就开始跑偏。问校园 GPU 实验室,它能一路扯到恋爱建议;上下文里明明只写了周一到周五,它还会顺手编出周六营业时间。
这里要拆开的,是一个很小、但很实用的 NVIDIA NIM Guardrails 做法,不用引入复杂框架,只额外加两层拦截。
一层放进 Prompt 里,先把模型能回答的范围限制住。另一层借助第二次 LLM 调用做 grounding check,检查答案里的事实是不是真的有检索上下文支持;如果不支持,就统一返回 fallback。
别再只靠 RAG 自觉:检索到了上下文,不等于答案就安全
很多人在做 RAG 时,注意力常会放在 chunk 切分、向量库、top-k 以及 embedding 模型这些方面。
这些内容当然都重要,但它们解决的其实只是一个问题:模型到底能看到什么。
它没有自动解决另一个问题:模型能不能乱写。
原文里的示例是一个 USC 校园助手。前两步已经完成了 NIM 调用,基于 embedding 的检索也已经处理好了。用户提出问题后,会先召回相关性最高的校园信息,再把这些内容交给模型来作答。
但这里有两个真实风险:
| | |
|---|
| | 检索器总会返回几个最像的 chunk,但不判断问题该不该答 |
| | |
guardrails 不能只停在表述层面。
“能答什么”和“答案有没有证据”,更适合分开设成两个关卡。
这套方案的链路可以压成四步:
-> retrieve top-k context-> scoped prompt: answer OR fallback-> grounding check: verify answer against context-> ship answer OR replace with fallback它没有引入新服务,也没有上复杂规则引擎。 核心思路是:
很多内部助手、校园助手这类应用,还有客服问答、文档问答,采用这样的复杂度已经比较合适。它并不是最高级别的安全体系,但也足够把那种“会一本正经胡说”的 Demo,朝真正可用的工具再推进一大步。
采用的是 NVIDIA NIM 的 OpenAI-compatible API,所以在 Python 代码里就可以直接使用 openai 客户端。
如果前两篇里的 NIM 和 RAG 代码已经有了,这一步就可以直接跳过。没有的话,也可以先用下面这段搭一个最小环境:
%pip install -q openai numpyfrom openai import OpenAIif not os.getenv("NVIDIA_API_KEY"):os.environ["NVIDIA_API_KEY"] = getpass.getpass("Paste your NVIDIA API key (starts with nvapi-): ")base_url="https://integrate.api.nvidia.com/v1",api_key=os.environ["NVIDIA_API_KEY"],MODEL = "meta/llama-3.1-8b-instruct"EMBED_MODEL = "nvidia/nv-embedqa-e5-v5"def ask(system_prompt, user_message):response = client.chat.completions.create({"role": "system", "content": system_prompt},{"role": "user", "content": user_message},return response.choices[0].message.content这里有几个点别改错:
| |
|---|
base_url | 指向 NVIDIA NIM 的兼容 OpenAI API 入口 |
NVIDIA_API_KEY | |
MODEL | |
EMBED_MODEL | |
接下来准备一个很小的校园知识库:
"title": "USC AI Club meeting","text": "The USC AI Club meets every Thursday at 5 PM in the engineering building, room 204.","title": "USC GPU lab hours","text": "The USC GPU computing lab is open Monday to Friday from 10 AM to 6 PM.","title": "NVIDIA Developer Program","text": "USC students can join the NVIDIA Developer Program for free.","title": "Next USC workshop","text": "The next USC AI Club workshop will cover Retrieval Augmented Generation (RAG).",再补上 embedding 和检索函数:
def embed_texts(texts, input_type="passage"):response = client.embeddings.create(extra_body={"input_type": input_type},return [np.array(item.embedding, dtype=np.float32) for item in response.data]def cosine_similarity(a, b):denom = np.linalg.norm(a) * np.linalg.norm(b)return float(np.dot(a, b) / denom)def retrieve_context(question, k=3):q_emb = embed_texts([question], input_type="query")[0]scored = [(cosine_similarity(q_emb, item["embedding"]), item) for item in knowledge_base]scored.sort(key=lambda p: p[0], reverse=True)return "\n".join(f"- {item['text']}" for _, item in scored[:k])embed_texts([item["text"] for item in knowledge_base], "passage"),到这里,RAG 已经先能跑了,不过 guardrails 暂时还没加上。
第一层:用 Scoped Prompt 把回答范围钉死
第一层 guardrail 的思路比较直接,就是先把模型的边界明确下来,再给它配一个固定的拒答语。
FALLBACK = "I don't have that information — check with the USC AI Club."SCOPED_SYSTEM_PROMPT_TEMPLATE = """You are a USC campus assistant for AI Club,GPU lab, NVIDIA program, workshop, office hour, robotics lab, and tutoring- Answer ONLY using the CONTEXT below.- If the user asks about anything outside this scope (e.g. weather, jokes,personal advice, code generation, general world knowledge), reply with- If the answer is not present in the context, reply with exactly: "{fallback}"- Do not invent names, dates, room numbers, links, passwords, schedules,policies, or instructions that are not in the context.它不只是“提示词技巧”,更像是在把三件很具体的事落到实处:
| |
|---|
| |
| |
| 把 room number、password、schedule 这类高风险字段列出来 |
固定 fallback 很重要。 如果每次拒答都换一种说法,后面的日志和指标,连同前端提示、自动化测试,处理时一般都会更麻烦。从工程实现来看,宁可让它显得稍微生硬一些,也不要每次都改得太随意。
Scoped Prompt 说到底也只是请求,模型还是可能不照着来。
第二层更适宜单独拆出来:把问题先连同检索上下文、模型答案,一并交给另一次 NIM 调用,让它只判断一个问题:
答案里的每个事实,是否都能被上下文直接支持? 代码如下:
def answer_is_grounded(question: str, context: str, answer: str) -> bool:"You are a strict grounding verifier. Read the CONTEXT and the ""ANSWER. Respond with only 'yes' or 'no'. Say 'yes' if every ""factual claim in the ANSWER is directly supported by the CONTEXT. ""Say 'no' otherwise — including if the ANSWER adds information not ""in the CONTEXT, even if that information sounds plausible."f"CONTEXT:\n{context}\n\n"f"QUESTION:\n{question}\n\n""Is every factual claim in the ANSWER supported by the CONTEXT?"return verdict.strip().lower().startswith("yes")这段代码有几个小心机:
当然,这个 verifier 本身也仍然是 LLM,所以它同样会有出错的情况。
如果放到生产系统里,不能只依靠它。还得继续叠加按字段核对这类硬校验,像房间号、日期这类信息,以及链接、金额、电话、密码这些字段,都要看它们是否确实出现在上下文里。不过把它当成一个成本不高的验货环节来使用,已经能拦下不少那种“听起来很合理”的幻觉。
把两层合起来:ask_guarded() 才是真正对外接口
现在把检索、回答、验真、拒答串起来:
def ask_guarded(question: str) -> str:context = retrieve_context(question)system_prompt = SCOPED_SYSTEM_PROMPT_TEMPLATE.format(answer = ask(system_prompt, question)if not answer_is_grounded(question, context, answer):测试四个问题:
"When does the USC AI Club meet?","Can you write my breakup text?","What is the wifi password?","What are the USC GPU lab Saturday hours?",print(f"A: {ask_guarded(question)}\n")预期结果大概是:
真正值得看的,是第四个问题。 很多模型会顺着给出“周六不开放”这类回答,也可能直接编出一个时间,这类说法只是听上去比较合理。可要是上下文里并没有写到,严格的 grounding check 通常就会把这类回答拦下来。
这种拒答方式对用户来说,可能会显得不够热情。可放到产品这一侧看,它反而更能体现出结果的可靠性。
这套方案很轻,但不是没有代价。 每个用户问题至少都会多出一次 verifier 调用。这个成本会直接增加,最好先把这一点考虑清楚:
我的建议是,不要一上来就全站启用。 先在高风险场景里用,比如:
如果只是做个闲聊助手,未必一定要卡得这么严;可一旦答案会直接影响用户操作,那这类成本,值得付出。
原来的方案其实是有意做得比较简单。真要上到生产环境,还得补上五件事:
| |
|---|
| verifier 返回 JSON,例如 {"grounded": true, "reason": "..."} |
| |
| |
| |
| |
尤其是日志。 Guardrails 拦下来的内容,不只是“失败请求”。很多时候,这类情况不只会让知识库还缺哪些内容看出来,也能说明用户到底想问什么,还能反过来看出 Prompt 在某些边界上的表述还不够清楚。
fallback 不必只当作结束来看。它很多时候也在提醒,后面还需要去补知识库、修改 Prompt,或者对产品再做调整。
NVIDIA NIM 这套示例更有价值的,并不是它选用了某个很高级的安全框架。 恰恰相反,它的价值在于足够朴素:
它更适宜把 Demo 往可控工具的方向推进,不适合拿来当作已经解决全部 AI 安全问题的方案。
真要开展严格的生产安全,还得先把权限和数据边界纳入,再把提示词注入、工具调用审批、人工复核、模型版本变更以及评测回归这些项单列覆盖。当前的 AI 应用要是还只是“检索完就直接回答”,先补上这两层,比裸跑已经强很多了。
别把 Guardrails 当补丁:它应该站在应用入口
我更愿意把这套方案理解成一个基本工程习惯: 模型不是答案会直接出去的那个口子,真正作为出口的,是 guarded function。
对外暴露的也不是 ask(),而是 ask_guarded()。用户的问题要先经过检索、范围限定,还有证据检查。之后才可以出现在界面上。
这条边界要是先立起来了,后面再补规则,做评测、留日志,以及加上人工审核,都会顺一些。
不要等到 AI 应用已经开始胡说了,再临时补安全话术。很关键。RAG 应用从第一天开始,就应该先问自己一个问题:
这句话到底有没有证据? 如果没有,就别让它说。