2026 睿抗 CAIP 大模型及智能体应用赛项决赛样题答案与备赛大观
按任务梳理 2026 睿抗 CAIP 大模型及智能体应用决赛样题的基础知识与 94 个填空的参考答案,覆盖 Conda、Transformers、LoRA、RAG、缓存降级与多智能体安全。
睿抗CAIP强脑赛道使用说明
这份答案是备赛时对照用的,不是标准答案。我的建议是:先背题目里的代码框架和指定参数,再回头看“基础知识”把概念对上;最后自己填一遍,展开答案对照。解析不只给结果,也说明为什么这么填,方便你回头补对应的知识点。
样题没有官方答案。下面这些内容是按题面、项目结构和公开文档推出来的;题面没给、只能靠附件常量判断的空,我标了“按题面推断”,用公开文档核对过的标了“网络核对”。如果你的附件和这里不一致,以附件为准。
※:题面没给常量,按上下文和常见实现推断。†:用公开文档核对过(Hugging Face / ModelScope / Qwen 文档)。- 代码答案只写空缺本身;字符串答案按题面约定可以省略引号。
资料索引
| 资料 | 适合查什么 | 本卷对应 |
|---|---|---|
ai-agent-interview-guide | Agent 基础、RAG、工具调用、多智能体、工程化 | 任务 5–10 的概念索引 |
Hello-Agents | 第 4、8、9、12 章:ReAct、记忆与 RAG、上下文工程、评估 | 任务 2、5、6、9、10 |
Hands-on-AA | 第 5、6、10 章:智能体架构、记忆/RAG、LoRA | 任务 4、5、6 |
dive-into-llms | 第 1、2、6、10 章:微调、提示学习、越狱、智能体安全 | 任务 1–4、10 |
agent-interview-hub | RAG、多 Agent、Function Calling 专题 | 任务 6–10 |
菜鸟教程 AI Agent | Agent 组成、Token、RAG、多智能体、工具调用、评估与部署的入门解释 | 任务 5、6、8–10 的通俗背景 |
读法:菜鸟教程看概念,Hello-Agents 补机制,两个面试仓库查工程细节,dive-into-llms / Hands-on-AA 看微调和安全实验。章节号以仓库当前目录为准;Hands-on-AA 是 notebook,路径形如 part_2/chapter_5。
面试八股文的入口在 docs/01-面试八股文/README.md。
任务地图
| 模块 | 任务 | 关键词 |
|---|---|---|
| 模型层 | 1 开发环境核验 | Conda、CPU wheel、依赖、日志 |
| 模型层 | 2 大模型服务部署 | ModelScope、Transformers、FastAPI、生成参数 |
| 模型层 | 3 日志清洗与样本构建 | 正则脱敏、数据清洗、9:1、messages |
| 模型层 | 4 LoRA 微调 | PEFT、秩、目标模块、TrainingArguments |
| 应用层 | 5 对话与摘要 | 多轮会话、角色、token、CLI |
| 应用层 | 6 政策知识 RAG | 切块、Embedding、FAISS、阈值 |
| 应用层 | 7 性能与体验优化 | 缓存、TTL、降级、性能字段 |
| 智能体层 | 8 工单角色设计 | Planner、Executor、QA、JSON 契约 |
| 智能体层 | 9 多工具流程 | 工具注册、JSONL、工具顺序 |
| 智能体层 | 10 编排与安全 | 规则路由、拦截、RBAC、审计 |
模块一:模型层:环境部署与 LoRA 微调
任务 1:开发环境核验与依赖检查
基础知识
Conda 环境就是一套独立的 Python 安装目录。建环境、激活、确认解释器位置:
conda create -n raicom python=3.11 -yconda activate raicomwhich pythonpython -c "import sys; print(sys.prefix)"创建和激活用同一个环境名即可,名字只决定目录。提示符变了不算数,which python 指向 envs/raicom/bin/python 才算。
torch 的 CPU 版和 GPU 版共用一个包名,区别在 wheel:
- 默认 PyPI 的 Linux wheel 通常带 CUDA 运行时,体积大;
--index-url https://download.pytorch.org/whl/cpu从 CPU 索引取包,wheel 不链接 CUDA;--index-url会替换默认索引,不是追加。用内网镜像时先确认它同步了whl/cpu。
pip install torch \ --index-url https://download.pytorch.org/whl/cpupython -c "import torch; print(torch.__version__, torch.cuda.is_available())"# 2.x.x+cpu Falserequirements.txt 锁的是直接依赖。Trainer 的设备放置、混合精度、分布式启动由 accelerate 负责,缺它会在 import 或初始化时报 ModuleNotFoundError;peft 的训练入口也依赖它。碰到这个错先查训练依赖,别去翻模型文件。
日志要能作为提交证据。tee 只接 stdout,脚本把错误写到 stderr 时会漏:
mkdir -p logsset -o pipefailpython env_check.py 2>&1 | tee logs/env_verify.logtee 不会自动建父目录,mkdir -p 先执行。pipefail 用来防止 Python 失败而 tee 成功、整条命令仍返回 0。
几个容易忽略的点:
conda activate提示未初始化:conda init bash,重开 shell。- 在 base 里
pip install:先确认which python,或用python -m pip。 - Python 3.11 装不上老版本
torch:查版本矩阵,别直接升级。
代码说明
| 代码 | 作用 |
|---|---|
conda create -n raicom python=3.11 -y | 建名为 raicom 的环境,指定 Python 3.11,-y 跳过确认 |
conda activate raicom | 激活环境,把它的 bin 目录放到 PATH 前面 |
which python | 查当前 python 的实际路径,确认没跑错环境 |
python -c "..." | 执行一段 Python 代码后退出,常用来做单行检查 |
pip install torch --index-url ... | 从指定索引安装 torch;行尾 \ 是换行续行 |
torch.__version__ | 版本字符串,CPU wheel 常带 +cpu |
torch.cuda.is_available() | 返回 True / False,CPU 环境应为 False |
python env_check.py | 运行环境检查脚本 |
2>&1 | 把 stderr 合并进 stdout,错误也会进日志 |
set -o pipefail | 管道里任一命令失败,整条命令返回非零 |
tee 和管道单独说:
|是管道,把左边命令的 stdout 交给右边命令的 stdin。tee logs/env_verify.log把 stdin 同时写到屏幕和文件;默认覆盖,要追加用tee -a。python env_check.py 2>&1 | tee logs/env_verify.log的完整含义:运行脚本,合并错误输出,再把全部输出一边打印一边写进日志。tee不会建目录,所以先mkdir -p logs。日志要保留多次运行时用tee -a:
python env_check.py 2>&1 | tee -a logs/env_verify.log题目 1.1(填空题)
创建并激活 Conda 环境,请写出对应命令。
# 1. 创建名为 raicom 的 Python 环境conda create -n __【1】__ python=3.11 -y# 2. 激活该环境conda activate __【2】__点击查看答案
答案:
【1】:raicom【2】:raicom
解析:Conda 用 -n 指定环境名,创建和激活必须同名,否则激活的是另一个环境;python=3.11 固定解释器版本。题面两空分别对应创建和激活同一条命令,所以都填 raicom。
题目 1.2(填空题)
安装 CPU 版 PyTorch,请写出安装命令中应使用的索引源(知识性考核,不需要实际执行)。
pip install torch --index-url __【3】__点击查看答案
答案: https://download.pytorch.org/whl/cpu
解析:PyTorch 的 CPU wheel 单独发布在 whl/cpu 索引;—index-url 是替换默认索引,不是追加。题面明确要求 CPU 版且只留一个索引参数,所以填 https://download.pytorch.org/whl/cpu。
题目 1.3(填空题)
附件 requirements.txt 中缺少一个 LoRA 微调必需的关键依赖(缺少它训练会报 ModuleNotFoundError),请补全依赖名。
transformers==4.51.3peft==0.15.2modelscope==1.25.0datasets==3.5.0sentence-transformers==3.4.1faiss-cpu==1.10.0fastapi==0.115.12uvicorn==0.34.2requests==2.32.3# 请补充缺少的依赖:__【4】__点击查看答案
答案: accelerate (网络核对)
解析:Trainer 的设备放置、混合精度、分布式启动由 Accelerate 负责,缺它时训练入口在 import/初始化阶段就报 ModuleNotFoundError;peft 的训练路径也依赖它。题面说“LoRA 微调必需、缺了会报 ModuleNotFoundError”,指向训练依赖而不是模型文件,所以填 accelerate。(网络核对:Hugging Face Transformers 文档)
题目 1.4(填空题)
运行环境校验脚本(附件 env_check.py),其日志会写入固定的落盘目录(相对于后端工程根目录)。请补全该目录。
pip install -r requirements.txtpython env_check.py | tee __【5】__/env_verify.log点击查看答案
答案: logs (按题面推断)
解析:tee 把 stdin 同时写到屏幕和文件,但不会创建父目录。题面说日志落在“相对于后端工程根目录的固定目录”,结合项目约定是 logs/;先 mkdir -p logs,tee logs/env_verify.log 才能成功。
任务 2:大模型服务部署与接口配置
基础知识
一次生成拆成三步:
inputs = tokenizer(prompt, return_tensors="pt")out = model.generate(**inputs, max_new_tokens=256, do_sample=False)text = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)tokenizer负责文本 ↔ token id;model根据 id 算下一个 token 的概率;decode把新生成的 id 变回文本。切片[输入长度:]是为了丢掉 prompt,skip_special_tokens=True去掉<|im_end|>这类标记。
model.eval() 关闭 dropout。推理时再加 torch.inference_mode()(或 no_grad()),不然中间激活会一直占内存。
模型仓库 ID 是“组织/模型”,例如 Qwen/Qwen3-0.6B。--local_dir 只决定文件放哪,不改变模型身份。本地目录至少要有 config.json、权重、tokenizer.json、generation_config.json;缺 tokenizer 时 from_pretrained 会直接报错。torch_dtype=torch.float32 在 CPU 上最稳,float16 / bfloat16 要看硬件。
Qwen 的对话格式由模板生成,不要手拼:
prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False,)add_generation_prompt=True 会在末尾补 assistant 的起始标记;不加,模型可能把整段当普通文本续写。enable_thinking=False 跳过 Qwen3 的思考段,CPU 上能少生成不少 token。
| 参数 | 作用 | 本题 |
|---|---|---|
max_new_tokens | 新生成的 token 上限,不含输入 | 256 |
do_sample | False 走贪心,结果可复现 | False |
temperature | 采样温度,仅 do_sample=True 时生效 | 不设 |
top_p | 核采样,仅采样时生效 | 不设 |
生成参数不止这几个,常见的还有:
| 参数 | 作用 |
|---|---|
top_k | 只在概率最高的 k 个 token 里采样,和 top_p 二选一或一起用 |
repetition_penalty | 惩罚已出现的 token,减轻复读 |
presence_penalty / frequency_penalty | OpenAI 风格:按“是否出现 / 出现次数”惩罚 |
stop / eos_token_id | 遇到指定字符串或 token 就停 |
stream | 流式返回,边生成边推送 |
不同接口的参数名不完全一样:
| 目的 | HF generate | OpenAI Chat Completions |
|---|---|---|
| 输出上限 | max_new_tokens | max_tokens / max_completion_tokens |
| 是否采样 | do_sample | 没有这个开关,靠 temperature / top_p |
| 温度 | temperature | temperature |
| 核采样 | top_p | top_p |
| 停止 | eos_token_id / stop_strings | stop |
| 流式 | TextIteratorStreamer | stream: true |
注意 do_sample=False 时 temperature、top_p 不生效,HF 直接走贪心;OpenAI 兼容接口通常用 temperature=0 近似贪心,但不同厂商实现有差异。
请求格式也有差别:
| 厂商/格式 | system 放哪 | 对话主体 |
|---|---|---|
| OpenAI Chat Completions | messages 里 role: "system" | messages |
| Anthropic Messages | 顶层 system 字段 | messages |
| Gemini generateContent | 无独立 system 字段,通常放首条 user | contents → parts |
比赛用 OpenAI/Qwen 格式,别把 system 提到顶层,也别用 contents / parts。
FastAPI 路由:
@app.post("/v1/chat/completions")async def chat(req: Request): body = await req.json() ...@app.post 只接受 POST;req: Request 适合消息体不固定的场景,结构固定时用 Pydantic 更稳。host="0.0.0.0" 监听所有网卡,127.0.0.1 只允许本机,端口要和评测脚本一致。
注意 async def 里直接调 model.generate 会阻塞事件循环。CPU 推理本来就慢,并发请求会被排队;要么把路由写成普通 def(FastAPI 放到线程池),要么用 run_in_threadpool,线上再加队列和超时。
接口测试:
URL=http://127.0.0.1:8700/v1/chat/completionscurl -s -X POST "$URL" \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"你好"}]}' \ -o submission/m1/api_test_result.json \ -w "%{http_code}"200 表示请求成功且业务正常;4xx 查请求参数、路径、鉴权;5xx 查服务端。连接被拒绝多半是端口没起,超时则是服务在但处理太慢。
代码说明
| 代码 | 作用 |
|---|---|
tokenizer(prompt, return_tensors="pt") | 文本转 token id,返回 PyTorch 张量字典 |
**inputs | 把字典拆成关键字参数,等价于 input_ids=..., attention_mask=... |
model.generate(...) | 自回归生成,返回包含输入和新 token 的完整序列 |
out[0] | 取 batch 里第一条序列 |
out[0][n:] | 切掉前 n 个输入 token,只留新生成部分 |
tokenizer.decode(..., skip_special_tokens=True) | id 转文本,去掉 im_end 等特殊标记 |
torch.inference_mode() | 关闭梯度记录,省内存 |
messages | 消息列表,元素是 {"role": ..., "content": ...} |
add_generation_prompt=True | 末尾补 assistant 起始标记 |
enable_thinking=False | Qwen3 关闭思考段 |
OpenAI 对话 JSON 的解析:
import json
raw = '{"messages":[{"role":"user","content":"你好"}]}'body = json.loads(raw) # 字符串 -> dictmessages = body["messages"] # listprint(messages[0]["role"]) # userprint(messages[0]["content"]) # 你好json.loads只接受字符串;body是普通 Python dict/list。body["messages"]取列表,messages[0]取第一条,["role"]取角色。- 反过来用
json.dumps(obj)把 dict 转成 JSON 字符串。 - JSON 的键和字符串必须双引号;Python dict 可以单引号,两者不能混。
FastAPI 里 await req.json() 做的就是 json.loads 这一步:把请求体读成 dict。
curl 参数:
| 参数 | 作用 |
|---|---|
-s | 静默模式,不显示进度 |
-X POST | 指定 HTTP 方法 |
-H "Content-Type: application/json" | 声明请求体是 JSON |
-d '...' | 发送请求体 |
-o 文件 | 把响应体写到文件 |
-w "%{http_code}" | 只打印状态码 |
题目 2.1(填空题)
比赛指定模型已就位(目录 ./models/Qwen3-0.6B)。请写出应使用的模型仓库 ID。
modelscope download --model __【6】__ --local_dir ./models/Qwen3-0.6B点击查看答案
答案: Qwen/Qwen3-0.6B (网络核对)
解析:ModelScope 的仓库 ID 是“组织/模型”两段式;—local_dir 只决定下载到哪,不改变模型身份。题面给的本地目录是 ./models/Qwen3-0.6B,对应官方仓库 Qwen/Qwen3-0.6B,所以填它。(网络核对:ModelScope 模型页)
题目 2.2(填空题)
编写推理服务 serve_model.py,将模型部署为 RESTful 服务。请补充代码空缺(生成参数按本题指定:长度上限 256、关闭采样)。
import torch, uvicornfrom fastapi import FastAPI, Requestfrom transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_PATH = "./models/Qwen3-0.6B"tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)model = AutoModelForCausalLM.__【7】__(MODEL_PATH, torch_dtype=torch.float32)model.eval()app = FastAPI()
@app.__【8】__("/v1/chat/completions")async def chat(req: Request): messages = (await req.json())["messages"] prompt = tokenizer.__【9】__( messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(prompt, return_tensors="pt") out = model.generate(**inputs, max_new_tokens=__【10】__, do_sample=__【11】__) ...
uvicorn.run(app, host="0.0.0.0", port=8700)点击查看答案
答案:
【7】:from_pretrained【8】:post【9】:apply_chat_template【10】:256【11】:False
解析:五个空对应推理服务的五个必要环节:from_pretrained 读权重和配置;@app.post 注册 POST 路由;apply_chat_template 按训练格式拼对话;题面指定长度上限 256,所以 max_new_tokens=256;题面要求关闭采样,do_sample=False 走贪心。
题目 2.3(填空题)
用 curl 完成接口连通性测试。请补充端口号与预期 HTTP 状态码。
curl -s -X POST http://127.0.0.1:__【12】__/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"你好"}]}' \ -o submission/m1/api_test_result.json -w "%{http_code}" # 预期状态码 __【13】__点击查看答案
答案:
【12】:8700【13】:200
解析:端口要和架构表一致,模型层是 :8700;请求体是合法 JSON、路由存在、服务正常时返回 200。题面用 -w ”%{http_code}” 只取状态码,所以填 8700 和 200。
题目 2.4(填空题)
Qwen3 支持“思考模式”。本卷要求在 CPU 上关闭思考模式。请写出关闭时所传的参数值(填 True 或 False)。
prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=__【14】__) # CPU 环境关闭思考以提速点击查看答案
答案: False (网络核对)
解析:Qwen3 的 chat template 支持 enable_thinking;开启后先输出思考段再给答案,关闭则跳过。题面明确说“CPU 环境关闭思考以提速”,所以填 False。(网络核对:Qwen 官方 Quickstart)
任务 3:咨询日志清洗与微调样本构建
基础知识
日志里的手机号、测试会话、空回答、重复提问都会进训练集。清洗的目标是让训练分布贴近真实咨询。
手机号按大陆号段匹配:
import rePHONE_RE = re.compile( r"(?<![0-9])1[3-9][0-9]{9}(?![0-9])")record["phone"] = PHONE_RE.sub("***", record["phone"])1[3-9]限定首位和第二位号段,后面 9 位数字;[0-9]比\d严格,避免匹配 Unicode 数字;- 前后 lookaround 防止从更长的数字串中间截一段;
- 占位符固定为
***或[PHONE],不要保留后四位。
清洗条件写成代码:
valid = [ r for r in data if not r["session_id"].startswith("test") and r["turns"] and all(t["a"].strip() for t in r["turns"])]all([]) 返回 True,所以没有 turns 的记录要单独用 r["turns"] 判空。重复样本按会话或内容哈希去重。
| 目标 | 规则 | 容易出错 |
|---|---|---|
| 去测试数据 | session_id 前缀 | 前缀过宽会误删真实会话 |
| 去空回答 | strip() 后非空 | all([]) 为真 |
| 去重 | 会话/内容哈希 | 只按问题去重会误删合理复问 |
| 脱敏 | 正则替换 | 过宽/过窄都会漏或误伤 |
划分前先清洗、去重,再按 9:1 切:
n_train = int(len(valid) * 0.9)train, val = valid[:n_train], valid[n_train:]顺序反了,同一通会话可能同时落在两边,验证集分数会虚高。划分前固定随机种子打乱;数据量很小时验证集只有几条,指标波动大,改用交叉验证或补数据。
SFT 样本用 messages:
{"messages": [ {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}role 只有 system、user、assistant(工具调用还有 tool)。训练时 loss 只算 assistant 的 token:prompt 段和 padding 的 labels 置 -100,否则模型会学复述问题。训练和推理用同一套 chat template,截断前先过滤超长样本,别把答案切掉。
代码说明
正则拆开看:
(?<![0-9]) 负向后顾:前一个字符不能是数字1 手机号首位固定为 1[3-9] 第二位 3 到 9[0-9]{9} 后面正好 9 位数字(?![0-9]) 负向前瞻:后一个字符不能是数字re.compile(...) 预编译正则;re.sub("***", ...) 把所有匹配替换成 ***。
Python 代码:
| 代码 | 作用 |
|---|---|
r["turns"] | 取列表;空列表在 if 里为假,顺手过滤空记录 |
t["a"].strip() | 去掉首尾空白;空字符串为假 |
all(...) | 所有元素都为真才返回 True;空迭代器返回 True |
session_id.startswith("test") | 判断前缀,命中测试会话 |
int(len(valid) * 0.9) | 取 90% 的整数条数,向下取整 |
valid[:n_train] | 前 90% |
valid[n_train:] | 剩下的 10% |
OpenAI/Qwen 样本的 JSON 解析:
import json
raw = '{"messages":[{"role":"user","content":"你好"}]}'obj = json.loads(raw)for msg in obj["messages"]: print(msg["role"], msg["content"])messages 是列表,顺序就是对话顺序;每条只有 role 和 content 两个常用字段。json.loads 管“字符串 → 对象”,json.dumps 管“对象 → 字符串”。
题目 3.1(填空题)
对手机号做脱敏处理。请补充正则与占位符。
import rePHONE_RE = re.compile(__【15】__) # 中国大陆手机号:11 位,号段 1[3-9] 开头record["phone"] = PHONE_RE.sub(__【16】__, record["phone"])点击查看答案
答案:
【15】:1[3-9]\d{9}【16】:***(按题面推断)
解析:大陆手机号 11 位:首位 1,第二位 3 到 9,后 9 位数字,正则是 1[3-9]\d{9}。re.sub 默认替换全部匹配;题面要求脱敏且不给可逆线索,占位符用固定的 ***。
题目 3.2(填空题)
过滤测试数据与空回答记录,并按 9:1 划分训练集与验证集。请补充两处空缺。
valid = [r for r in data if not r["session_id"].startswith(__【17】__) and all(t["a"].strip() for t in r["turns"])]n_train = int(len(valid) * __【18】__) # 9:1 划分点击查看答案
答案:
【17】:test【18】:0.9
解析:startswith(“test”) 过滤测试会话;all(…) 要求每轮回答 strip() 后非空;int(len(valid) * 0.9) 取 90% 训练、10% 验证。题面明确 9:1,所以填 test 和 0.9。
题目 3.3(填空题)
将有效记录转换为指令微调标准格式(OpenAI/Qwen 对话格式)。请补充顶层字段名。
{"__【19】__": [{"role":"user","content":"..."}, {"role":"assistant","content":"..."}]}点击查看答案
答案: messages
解析:OpenAI/Qwen 的指令微调样本用顶层 messages,值是 role / content 数组;多轮按 user/assistant 交替。题面给出的 JSON 骨架只有一个顶层键,所以填 messages。
任务 4:LoRA 小规模微调与效果验证
基础知识
全参数微调要更新所有权重,还要存梯度和优化器状态,0.6B 模型在 CPU 上基本跑不动。LoRA 冻结原权重,只训练低秩增量:
W' = W + (alpha / r) · B AA: r × k B: d × r可训练参数: r × (d + k) 原参数: d × kA 随机初始化,B 置零,训练开始时 B A = 0,旁路不影响原模型。两个都置零会出事:B 有梯度,但 A 的梯度里带 B^T,恒为零,A 永远不更新。
Qwen3-0.6B 的实际尺寸:hidden 1024,16 个注意力头,8 个 KV 头,head_dim 128,28 层。q_proj 1024→2048,k_proj / v_proj 1024→1024,o_proj 2048→1024。取 r=8:
q_proj: 8×(1024+2048) = 24,576k_proj: 8×(1024+1024) = 16,384v_proj: 8×(1024+1024) = 16,384o_proj: 8×(2048+1024) = 24,576每层合计: 81,92028 层: 2,293,760 ≈ 229 万占 7.5 亿总参数: 约 0.3%适配器 fp32 不到 10 MB;全参数微调光权重就是数 GB。内存不够时先量化基座:
| 精度 | 0.6B 权重 | 说明 |
|---|---|---|
| FP32 | 约 3 GB | CPU 默认,最稳 |
| FP16 / BF16 | 约 1.5 GB | 需要硬件支持 |
| INT8 | 约 0.75 GB | 量化推理,精度略降 |
| INT4 | 约 0.4 GB | QLoRA / 低显存 |
QLoRA = 4-bit 量化基座 + LoRA 旁路,训练时只更新旁路。量化能省内存,但 CPU 上不一定更快,取决于算子支持;比赛先保证 float32 能跑通。PEFT 的入口:
from peft import LoraConfig, get_peft_model
cfg = LoraConfig( task_type="CAUSAL_LM", r=8, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05)model = get_peft_model(model, cfg)target_modules 的名字必须和模型里的模块名一致。写错不会报错,只会没有可训练参数或效果很差;用 dict(model.named_modules()) 核对。只加 q_proj、v_proj 更省参数,四个都加表达能力强一些。
| 参数 | 作用 | 常用值 |
|---|---|---|
r | 秩,越大容量越大 | 8 |
lora_alpha | 缩放,常取 2r 到 4r | 16 |
lora_dropout | 防过拟合 | 0.05 |
learning_rate | LoRA 可比全参微调大 | 1e-4 到 2e-4 |
num_train_epochs | 遍历数据集的次数 | 1 |
use_cpu | 强制 CPU | True |
CPU 训练先保证流程跑通、适配器能保存,再看 loss 和生成效果。训练完只存适配器:
model.save_pretrained("adapter")# adapter_config.json + adapter_model.safetensors加载时先加载基座,再 PeftModel.from_pretrained(base, "adapter");部署前可 merge_and_unload() 合回原权重。合并后不能再单独卸载,训练产物要留。
容易出错的地方:target_modules 写错;只存适配器忘了 tokenizer;训练/推理模板不一致;学习率过大导致 loss 震荡;CPU 上开 fp16 报错;device_map 和 use_cpu 冲突。
代码说明
| 代码 | 作用 |
|---|---|
LoraConfig(...) | 定义旁路结构:任务类型、秩、缩放、目标模块、dropout |
task_type="CAUSAL_LM" | 自回归语言模型 |
r=8 | 低秩矩阵的秩 |
lora_alpha=16 | 缩放系数,实际缩放约为 alpha/r |
target_modules=[...] | 指定把旁路加到哪些线性层 |
get_peft_model(model, cfg) | 冻结原权重,按配置插入 LoRA 层 |
model.print_trainable_parameters() | 打印可训练参数占比,确认配置生效 |
TrainingArguments(...) | 训练超参:输出目录、轮数、批大小、学习率、日志间隔、设备 |
Trainer(...).train() | 执行训练循环 |
model.save_pretrained("adapter") | 只保存适配器权重和配置 |
PeftModel.from_pretrained(base, "adapter") | 在基座上加载适配器 |
merge_and_unload() | 把旁路合回原权重,得到普通模型 |
题目 4.1(填空题)
使用 PEFT 对 Qwen3-0.6B 做 LoRA 微调。请补充配置代码(秩按本题指定为 8)。
from peft import LoraConfig, get_peft_modellora_cfg = __【20】__(task_type="CAUSAL_LM", r=__【21】__, lora_alpha=16, target_modules=["q_proj","k_proj","v_proj","o_proj"], lora_dropout=0.05)model = __【22】__(model, lora_cfg)点击查看答案
答案:
【20】:LoraConfig【21】:8【22】:get_peft_model
解析:PEFT 用 LoraConfig 描述旁路:r 是秩、lora_alpha 是缩放、target_modules 指定加在 q/k/v/o 投影;get_peft_model 把配置注入模型并冻结原权重。题面指定秩为 8,所以三个空是 LoraConfig、8、get_peft_model。
题目 4.2(填空题)
为控制 CPU 训练时间,请补充训练参数(只训练 1 轮、在 CPU 上训练)。
training_args = TrainingArguments( output_dir="submission/m1/finetune", num_train_epochs=__【23】__, per_device_train_batch_size=2, learning_rate=2e-4, logging_steps=10, use_cpu=__【24】__)点击查看答案
答案:
【23】:1【24】:True
解析:num_train_epochs 控制遍历数据集的次数,题面要求只训练 1 轮;use_cpu=True 强制 CPU,题面明确 CPU 训练。CPU 上先保证流程跑通、适配器能保存,再谈效果,所以填 1 和 True。
模块二:应用层:馆内服务助手开发
任务 5:对话与摘要能力实现
基础知识
模型本身不记对话,历史放在服务进程里:
HISTORY = {}history = HISTORY.setdefault(sid, [])history.append({"role": "user", "content": text})...history.append({"role": "assistant", "content": answer})setdefault 在键不存在时写入空列表并返回,省掉一次判断。每轮两条消息,user 一条、assistant 一条。
| 角色 | 谁写 | 放什么 |
|---|---|---|
system | 开发者 | 行为约束、任务指令、输出格式 |
user | 用户 | 问题、待处理的素材 |
assistant | 模型 | 回复、工具调用前的思考 |
tool | 运行时 | 工具返回结果 |
摘要接口把“请生成 100 字以内摘要”放 system,公告正文放 user。指令和素材混在一起,模型容易把指令也当成要摘要的内容。
历史不能一直追加。上下文有上限,token 越多越慢,注意力也会被稀释。滑动窗口保留最近 N 条:
MAX_HISTORY_MESSAGES = 4 # 2 轮recent = history[-MAX_HISTORY_MESSAGES:]上下文窗口是输入 + 输出共享的上限。max_new_tokens 只是输出预留,不是总预算。几个物理限制:
- KV Cache 随序列长度线性增长,注意力计算随长度平方增长;长上下文更吃显存、更慢。
- 长上下文会出现“中间遗忘”:关键信息在长文档中间更容易被漏掉,窗口大不等于都能记住。
- 标称窗口不等于实际可用窗口,有的产品会用滑动窗口或截断,实际记忆更短。
- 预算分配:system + 最近几轮 + 检索片段 + 输出预留;超了先压缩历史或减少检索块,别硬塞。
一轮两条,保留两轮就是 4 条。更稳的做法是 system 常驻 + 最近几轮原文 + 更早内容的摘要,摘要里保留任务状态、用户偏好和未解决问题。
max_new_tokens 的单位是 token,不是汉字。一个汉字可能对应多个 token,“100 字摘要”不能写成 max_new_tokens=100,256 也不保证输出 256 个汉字。按字符截断:
answer = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)if len(answer) > 128: answer = answer[:128]len 数的是 Unicode 码点,不是字节。解码时切掉输入部分,否则回复里会带上一整段提问。
摘要还有两个问题:公告太长要分段摘要再合并;模型可能编造公告里没有的内容,提示词里写“只依据给定公告,找不到就说不知道”,输出后再核对关键实体。
命令行联调:
text = input("请输入问题(q 退出):")url = "http://127.0.0.1:8100/chat"r = requests.post(url, json={"session_id": "cli", "text": text}, timeout=30).json()print(r["reply"])线上脚本要加 timeout 和异常处理,服务挂起时客户端不会一直等。
容易踩的坑:生成失败还把空回复写进历史;多请求共用一个 HISTORY 没加锁;历史无限增长;滑动窗口把 system 挤掉;async def 里同步调 generate。
代码说明
| 代码 | 作用 |
|---|---|
HISTORY.setdefault(sid, []) | 键存在就返回原列表;不存在就写入空列表再返回 |
history.append({...}) | 把一条消息追加到会话列表 |
history[-4:] | 取最后 4 条;负数下标从末尾数 |
tokenizer.decode(...) | token id 转文本 |
out[0][n:] | 切掉输入,只留新生成 |
len(answer) | 字符数,不是 token 数 |
input("...") | 读一行命令行输入,返回字符串 |
requests.post(url, json=...) | 发 POST,json= 会自动序列化并设 Content-Type |
.json() | 把响应体解析成 dict |
请求和响应长这样:
{"session_id": "cli", "text": "借阅卡怎么补办?"}{"session_id": "cli", "reply": "带学生证和照片到总服务台办理。"}requests.post(..., json=payload) 等价于 data=json.dumps(payload) 再加 JSON 头;用 json= 更省事。.json() 内部就是 json.loads(response.text)。
题目 5.1(填空题)
实现多轮对话接口 /chat。请补充代码空缺(生成参数按本题指定:长度上限 256、关闭采样)。
HISTORY = {}
@app.__【25】__("/chat")async def chat(req: Request): body = await req.json() sid, text = body["session_id"], body["text"] history = HISTORY.__【26】__(sid, []) # 会话历史不存在则创建 history.append({"role": "user", "content": text}) prompt = tokenizer.apply_chat_template(history, tokenize=False, add_generation_prompt=True, enable_thinking=False) inputs = tokenizer(prompt, return_tensors="pt") out = model.generate(**inputs, max_new_tokens=__【27】__, do_sample=__【28】__) answer = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) history.append({"role": __【29】__, "content": answer}) return {"session_id": sid, "reply": answer}点击查看答案
答案:
【25】:post【26】:setdefault【27】:256【28】:False【29】:assistant
解析:@app.post 注册 POST;HISTORY.setdefault(sid, []) 在会话不存在时建空列表并返回;生成参数与前面一致,题面指定 256、关闭采样;回复由模型生成,角色是 assistant。所以五个空依次是 post、setdefault、256、False、assistant。
题目 5.2(填空题)
实现通知摘要接口 /summarize。摘要指令「请生成 100 字以内摘要」应放在 __【30】__ 角色的消息中(填英文角色名);其生成长度上限(本题指定)为 __【31】__。
messages = [ {"role": "__【30】__", "content": "请生成100字以内摘要"}, {"role": "user", "content": announcement},]out = model.generate(**inputs, max_new_tokens=__【31】__, do_sample=False)点击查看答案
答案:
【30】:system【31】:256(按题面推断)
解析:system 承载行为约束和任务指令,user 放待处理素材。题面要求“请生成 100 字以内摘要”,这是指令,应放 system;生成上限沿用本题指定的 256,给 100 字留 token 余量。
题目 5.3(填空题)
编写命令行联调脚本 cli.py。请补充两个内置/库函数名。
while True: text = __【32】__("请输入问题(输入q退出):") if text == "q": break r = requests.__【33】__("http://127.0.0.1:8100/chat", json={"session_id": "cli", "text": text}).json() print(r["reply"])点击查看答案
答案:
【32】:input【33】:post
解析:input() 读一行命令行输入;requests.post(url, json=…) 发 JSON。题面路由是 /chat 且只接受 POST,所以不能用 requests.get,两个空是 input 和 post。
题目 5.4(填空题)
max_new_tokens 的计数单位是 __【34】__(英文单词单数,含义为“词元/令牌”;提示:它并不等同于中文字数);若要在返回前统计摘要的字符数,可用的 Python 内置函数是 __【35】__。
out = model.generate(**inputs, max_new_tokens=128, do_sample=False)answer = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)if __【35】__(answer) > 128: # 字符数校验 answer = answer[:128]点击查看答案
答案:
【34】:token【35】:len
解析:max_new_tokens 数的是 token,不是汉字;一个汉字可能对应多个 token。len() 数 Python 字符串的码点,能用来做字符数校验。题面问单位和统计字符数的内置函数,所以填 token 和 len。
题目 5.5(填空题)
多轮对话中 HISTORY[sid] 每轮写入两条消息(user 提问+assistant 回复)。若代码只保留最近 __【36】__ 条消息,即保留最近 2 轮上下文(填数字)。
MAX_HISTORY_MESSAGES = __【36】__ # 保留最近 2 轮recent = history[-MAX_HISTORY_MESSAGES:]点击查看答案
答案: 4
解析:每轮写入 user + assistant 两条消息;题面要求保留最近 2 轮,所以窗口是 2 × 2 = 4 条。
任务 6:政策知识接入与检索增强(RAG)
基础知识
RAG 先在文档里找证据,再把证据和问题一起交给模型。它适合知识有截止时间、私有文档不在训练集、回答需要引用来源的场景。RAG 不是长期记忆,也不是微调的替代品。
流程分两段:
- 离线:加载文档 → 清洗 → 切块 → 向量化 → 写索引。
- 在线:问题向量化 → 检索 Top-K → 可选重排 → 拼提示 → 生成 → 引用。
RAG、微调、长上下文不是三选一:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RAG | 运行时检索外部知识 | 知识可更新、可溯源、成本低 | 检索质量决定上限 |
| 微调 | 用数据改权重 | 风格/格式稳定,推理快 | 成本高,知识会过时,可能遗忘 |
| 长上下文 | 文档全塞进窗口 | 实现简单 | token 贵,长文本容易漏信息 |
常见组合是 RAG 提供事实、微调管格式和风格、长上下文处理单次大文档。
RAG 的演进路线:
- Naive RAG:检索 → 拼接 → 生成。
- Advanced RAG:查询改写、向量 + BM25 混合检索、重排序、自适应检索。
- Modular RAG:按需组合检索模块。
- Graph RAG:用知识图谱补多跳关系。
- Agentic RAG:由 Agent 决定何时检索、检索什么、要不要多轮检索。
切块大小是第一个取舍。块太大,噪声多,向量被主题稀释;块太小,定义、条件、指代被切断。按自然段 \n\n 切、每块不超过 500 字,是个能解释的基线;按标题层级切更好,章节路径可以放进元数据。重叠取块大小的 10%–20%,能缓解边界丢失,但会增加索引和重复召回。
嵌入用同一个模型处理查询和文档:
from sentence_transformers import SentenceTransformer
emb = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 512 维vecs = emb.encode(chunks)BGE 的检索任务通常给查询加指令前缀(如“为这个句子生成表示以用于检索相关文章:”),文档不加。是否加按模型卡和实测决定,但查询和文档的处理必须一致。模型不一致,向量不在一个空间,分数没有意义。
FAISS 建索引:
import faiss
faiss.normalize_L2(vecs)index = faiss.IndexFlatIP(512)index.add(vecs)faiss.write_index(index, "submission/m2/rag/policy.index")IndexFlatIP 算内积。内积等于余弦相似度有个前提:两个向量都做了 L2 归一化。查询向量也要归一化。index.ntotal 是向量条数,write_index / read_index 负责持久化。小政策库用 Flat 精确检索就够,百万级以上再考虑 IVF、HNSW。
余弦的定义:
cos(q, d) = (q · d) / (|q| |d|)归一化后 |q| = |d| = 1,内积就是余弦。忘了归一化,内积会偏向长向量,阈值也跟着失效。
阈值用来拒答:
if best_score < threshold: return {"answer": "未检索到相关政策资料,请咨询图书馆总服务台。"}最高分低于阈值,说明知识库里没有足够相关的证据,别让模型自由发挥。阈值太高会误拒,太低会放过不相关内容,要拿正负样本调。更稳的是检索分数、重排分数和规则一起判断。
| 环节 | 关键选择 | 常见问题 |
|---|---|---|
| 切块 | 段落/标题,500 字基线,10%–20% 重叠 | 太大噪声多,太小切断语义 |
| 嵌入 | 同一模型、同一语言、同一归一化 | 查询和文档模型不一致 |
| 索引 | 小库 Flat,大库 IVF/HNSW | 忘了归一化 |
| 检索 | Top-K + 阈值 + 可选重排 | K 太大引入噪声,阈值太死 |
| 生成 | 证据分区、要求引用、允许拒答 | 把检索结果当指令执行 |
索引只存向量,不存原文。切块列表和索引一起持久化,检索到下标后用它取文本。索引和切块要同版本更新,否则 ntotal 和 len(chunks) 对不上,取回的文本会错位。
评估分两段:检索看 Recall@K、MRR、命中率;生成看忠实度、答案相关性、引用是否正确。只测最终答案,分不清是没检索到,还是检索到了但生成错了。
代码说明
| 代码 | 作用 |
|---|---|
SentenceTransformer("BAAI/bge-small-zh-v1.5") | 加载嵌入模型 |
emb.encode(chunks) | 批量编码,返回 (块数, 512) 的数组 |
faiss.normalize_L2(vecs) | 原地做 L2 归一化,让内积等于余弦 |
faiss.IndexFlatIP(512) | 建精确内积索引,维度必须等于向量维度 |
index.add(vecs) | 写入向量,顺序对应 chunks 下标 |
index.search(qvec, k) | 返回 D(分数)和 I(下标),形状都是 (1, k) |
index.ntotal | 索引里的向量条数 |
faiss.write_index(index, path) | 索引落盘 |
faiss.read_index(path) | 从磁盘加载索引 |
检索结果这样取:
D, I = index.search(qvec, 3)for score, idx in zip(D[0], I[0]): print(round(float(score), 4), chunks[idx]["text"][:50])D[0] 是第一条查询的分数,I[0] 是对应的切块下标。IndexFlatIP 返回的内积分数越大越相似;归一化后范围大致在 [-1, 1]。
题目 6.1(填空题)
对 docs/ 下 3 篇政策文档做切分,按自然段落切块且每块不超过 500 字。请补充默认参数与切分方式。
def split_doc(text: str, max_len=__【37】__): blocks = text.split(__【38】__) # 按自然段落切分 ...点击查看答案
答案:
【37】:500【38】:\n\n
解析:切块要在“噪声”和“语义完整”之间取舍。题面要求按自然段切、每块不超过 500 字,所以 max_len=500,text.split(“\n\n”) 按空行分段。
题目 6.2(填空题)
使用赛题指定中文嵌入模型向量化并建立 FAISS 索引。请补充模型 ID、归一化方法调用与索引维度。
from sentence_transformers import SentenceTransformerimport faissemb = SentenceTransformer("__【39】__")vecs = emb.encode(chunks)faiss.__【40】__(vecs) # 归一化,使内积等价余弦相似度index = faiss.IndexFlatIP(__【41】__) # bge-small-zh-v1.5 向量维度index.add(vecs)点击查看答案
答案:
【39】:BAAI/bge-small-zh-v1.5(网络核对)【40】:normalize_L2【41】:512(网络核对)
解析:BGE 中文小模型 BAAI/bge-small-zh-v1.5 输出 512 维;IndexFlatIP 算内积,只有先做 L2 归一化,内积才等于余弦相似度。题面指定该模型并要求内积等价余弦,所以三个空是模型 ID、normalize_L2、512。(网络核对:Hugging Face 模型卡)
题目 6.3(填空题)
实现检索增强问答。请补全检索块数(本题指定 3 个),并写出该技术的英文简称(三个字母,即“检索增强生成”)。
def rag_answer(question: str, top_k=__【42】__): scores, ids = index.search(qvec, top_k) contexts = [chunks[i]["text"] for i in ids[0]] ... # 检索结果拼入提示词再交由模型生成,该技术简称 __【43】__点击查看答案
答案:
【42】:3【43】:RAG
解析:题面指定检索 3 个块,所以 top_k=3;把检索结果拼进提示词再生成,这个技术的缩写是 RAG。两个空分别对应“检索块数”和“技术简称”。
题目 6.4(填空题)
持久化索引与切分块,文件名按提交规范命名。请补充索引文件名;并在保存前用断言保证“向量条数=切分块数”,该内建属性名是 index.__【45】__。
faiss.write_index(index, "submission/m2/rag/__【44】__")assert index.__【45】__ == len(chunks), "向量条数与切分块数不一致"点击查看答案
答案:
【44】:policy.index【45】:ntotal
解析:index.ntotal 是索引里的向量条数,用来断言等于切块数;题面要求按提交规范命名,索引文件是 policy.index。所以填 policy.index 和 ntotal。
题目 6.5(填空题)
要使测试问“借阅卡补办需要什么材料?”的检索回答同时含“学生证”与“照片”两个关键词,检索应命中的知识文档是 docs/ 目录下的 __【46】__(文件名含扩展名)。
question = "借阅卡补办需要什么材料?"answer = rag_answer(question) # 命中文档:docs/__【46】__点击查看答案
答案: 借阅卡补办.md (按题面推断)
解析:问题问“借阅卡补办需要什么材料”,应命中讲补办流程的文档;该文档包含“学生证”和“照片”。题面限定 docs/ 下的文件名,按上下文取 借阅卡补办.md,实际以附件目录为准。
题目 6.6(填空题)
超纲问题(如“明天天气怎么样?”)应友好拒答:当检索最高得分低于阈值时直接返回拒答文案。请补充判断条件中的比较运算符,以及加载已持久化索引的方法名。
threshold = 0.5best_score = float(scores[0][0])if best_score __【47】__ threshold: # 低于阈值则拒答 return {"answer": "未检索到相关政策资料,请咨询图书馆总服务台。"}...index = faiss.__【48】__("submission/m2/rag/policy.index")点击查看答案
答案:
【47】:<【48】:read_index
解析:题面要求最高分低于阈值时拒答,所以条件是 best_score < threshold;同时要求加载已持久化的 FAISS 索引,用 faiss.read_index,不要重新建 IndexFlatIP。
任务 7:应用性能与用户体验优化
基础知识
缓存的键由问题内容决定,不要只用会话 id:
key = hashlib.md5(question.encode("utf-8")).hexdigest()if key in CACHE and time.time() - CACHE[key][0] < TTL: return CACHE[key][1]...CACHE[key] = (time.time(), answer)命中要同时满足两个条件:键存在,且写入时间在 TTL 内。写入和读取用同一个 key,否则永远命中不了。
键里要带上影响答案的所有因素:规范化后的问题、租户/权限范围、模型版本、提示词版本、检索库版本。只哈希问题,模型或知识库更新后会返回旧答案。问题先做规范化(去首尾空白、统一大小写、全角转半角、去掉无意义标点),不然“借阅卡补办”和“借阅卡补办?”是两个键。
md5 不是加密算法,抗碰撞性早被攻破。做缓存键够用;键会被外部构造或需要防篡改时,换 sha256。hexdigest() 得到十六进制字符串,digest() 是 bytes。
| 策略 | 作用 | 风险 |
|---|---|---|
| 精确哈希缓存 | 命中直接返回,零推理成本 | 问题改写后不命中 |
| 语义缓存 | 相似问题也能命中 | 阈值低会返回错答案 |
| TTL | 限制答案过期时间 | 太短命中率低,太长不新鲜 |
| LRU/容量上限 | 防止内存无限增长 | 淘汰热点键会抖动 |
| 主动失效 | 知识库更新后清理 | 需要版本号或事件通知 |
错误响应、空答案、个性化内容不要缓存。高并发下防缓存击穿:同一个键同时未命中,多个请求一起回源;用单飞(single-flight)或分布式锁,只让一个请求生成,其余等结果。
模型服务超时、5xx、限流或熔断时走降级:
except Exception: return JSONResponse( content={"reply": "系统繁忙,请稍后重试。", "fallback": True}, status_code=200)这里用 200,表示业务已经处理了异常;用 5xx 会让客户端重试、监控报警,反而放大故障。fallback: true 让调用方知道这不是正常回答。
熔断和限流别混:
- 429 是限流,按
Retry-After或指数退避重试,加随机抖动,别立刻打满。 - 熔断器三态:Closed 正常放行;Open 直接失败、不再请求下游;Half-Open 放少量探测,连续成功再回 Closed。
- 重试只针对可恢复错误(超时、429、5xx);4xx 参数错误重试也没用。
但 200 不是无条件的。完全给不出有意义的结果、客户端应该重试时,用 503 加 Retry-After 更诚实。降级和故障要分开统计。
性能脚本对比 off / on 两组,字段名通常写成 avg_ms_off / avg_ms_on。同一问题连续请求 n 次,第 1 次未命中,第 2 次开始命中,所以“从第几次起全部命中”是 2。--n 的默认值看 argparse 配置,题面显式传 15 不代表默认是 15。
只报平均值不够。平均值会被少数慢请求拉高,也掩盖长尾;至少看 p50、p95、p99,区分冷启动和热缓存。测速固定输入、输出长度、并发度和硬件。
返回体字段是契约:摘要字符数 char_count、缓存命中 cache_hit、相似度列表 scores。字段名定下后,前端和评测脚本都按它取值,改字段名要版本化。
代码说明
| 代码 | 作用 |
|---|---|
question.encode("utf-8") | 字符串转 bytes,哈希函数只收 bytes |
hashlib.md5(...) | 建 MD5 哈希对象 |
.hexdigest() | 取 32 位十六进制字符串,适合做字典键 |
time.time() | 当前时间戳,单位秒,浮点数 |
CACHE[key] = (time.time(), answer) | 值存“写入时间 + 答案” |
time.time() - CACHE[key][0] < TTL | 判断是否过期 |
JSONResponse(content=..., status_code=...) | 同时指定响应体和状态码 |
except Exception: | 捕获所有异常,走降级分支 |
md5(...).hexdigest() 可以拆成三步:encode 转 bytes → md5 算摘要 → hexdigest 转十六进制字符串。缓存里存 tuple,[0] 是时间,[1] 是答案。
题目 7.1(填空题)
为高频重复问题增加缓存。请补充哈希方法(本题指定 md5)、缓存有效期(秒)与写入缓存时使用的键。
import hashlib, timeCACHE = {}key = hashlib.__【49】__(question.encode("utf-8")).hexdigest()if key in CACHE and time.time() - CACHE[key][0] < __【50】__: # 有效期 return CACHE[key][1]...CACHE[__【51】__] = (time.time(), answer)点击查看答案
答案:
【49】:md5【50】:300(按题面推断)【51】:key
解析:缓存键要由问题内容决定,md5(…).hexdigest() 把问题压成固定长度键;命中要同时满足键存在且未过 TTL。题面指定 md5,TTL 取常见起点 300 秒;写入必须用同一个 key,否则永远命中不了。
题目 7.2(填空题)
模型服务异常时返回友好降级提示,返回体中用 __【52】__ 字段标记本次为降级回复(本题指定该字段名);同时保持 HTTP 状态码为 __【53】__。
except Exception: return JSONResponse( content={"reply": "系统繁忙,请稍后重试,或致电图书馆总服务台6600。", "__【52】__": True}, status_code=__【53】__)点击查看答案
答案:
【52】:fallback【53】:200
解析:降级是业务层已经处理过的异常,不是服务器没处理完;用 200 避免客户端重试和监控误报。题面指定用 fallback 字段标记,所以填 fallback 和 200。
题目 7.3(填空题)
运行性能对比脚本(附件 perf_check.py),结果中衡量优化后平均响应时间的字段是 __【54】__;衡量优化前平均响应时间的字段是 __【55】__。
python perf_check.py --n 15 | tee submission/m2/perf_compare.json# 输出示例:{"avg_ms_off": 41.2, "__【54】__": 18.7, "n": 15}点击查看答案
答案:
【54】:avg_ms_on【55】:avg_ms_off
解析:性能脚本对比 off/on 两组,字段名要能一眼看出组别。题面示例已给 avg_ms_off 是优化前,优化后对应 avg_ms_on,所以两个空是 avg_ms_on 和 avg_ms_off。
题目 7.4(填空题)
perf_check.py 的 on 组对同一问题连续请求 n 次以验证缓存命中:第 1 次未命中,从第 __【56】__ 次起全部命中缓存(填数字)。另:该脚本命令行参数 --n 的默认取值为 __【57】__(填数字,见附件 argparse 配置)。
on = [timed_rag("借阅卡补办需要什么材料?") for _ in range(args.n)]点击查看答案
答案:
【56】:2【57】:10(按题面推断)
解析:第 1 次缓存未命中,写入后第 2 次开始命中,所以从第 2 次起全部命中。—n 的默认值看 argparse 配置,题面示例显式传 15 不代表默认是 15;按常见配置取 10。
题目 7.5(填空题)
降级响应既要有降级标记,又要保持 HTTP 状态码 200,可借助 FastAPI 显式构造响应对象:return __【58】__(content={...}, status_code=200)。请补充类名。
from fastapi.responses import __【58】__return __【58】__(content={"reply": "...", "fallback": True}, status_code=200)点击查看答案
答案: JSONResponse
解析:JSONResponse 可以同时指定响应体和状态码;HTTPException 会走 4xx/5xx。题面要求保持 200,所以用 JSONResponse。
题目 7.6(填空题)
为便于核对摘要长度与显示缓存状态,请补充返回体字段名:摘要字符数用 __【59】__ 字段;本次是否命中缓存用 __【60】__ 字段;检索到的相似度列表用 __【61】__ 字段(见应用实现)。
return JSONResponse({"summary": answer, "__【59】__": len(answer)})...return JSONResponse({... "__【60】__": True}) # 命中缓存...return JSONResponse({... "__【61】__": [round(s, 4) for s in scores]})点击查看答案
答案:
【59】:char_count(按题面推断)【60】:cache_hit(按题面推断)【61】:scores(按题面推断)
解析:返回体字段是接口契约:摘要字符数 char_count、缓存命中 cache_hit、相似度列表 scores。题面按这三个含义留空,填错会导致前端或评测脚本取不到值。
模块三:智能体层:多智能体编排与安全管控
任务 8:运维工单智能体角色设计
基础知识
多智能体就是把工单拆成几步,交给不同角色处理。工单流程里三个角色:
| 角色 | 职责 | 输入 | 输出 | 工具权限 |
|---|---|---|---|---|
| Planner | 解析意图、拆步骤 | 工单、历史 | intent、steps | 只读、规划类 |
| Executor | 按计划调工具 | steps | results、draft | 业务工具白名单 |
| QA | 质检、合规复核、面向用户 | draft、证据 | final、问题清单 | 只读、审核类 |
拆不拆角色,看子任务是否需要独立的推理、上下文、工具权限,或者能不能并行。确定的数据转换用函数或工作流节点就行;多一个 Agent 就多一次模型调用、多一层延迟和错误传播。
角色之间传 JSON,不传自然语言:
{ "ticket_id": "T2001", "intent": "repair", "steps": [ {"tool": "shelf_query", "args": {"room": "R305"}} ]}steps 是有序数组,tool 是工具名,args 是参数。执行日志至少记 tool、args、result:调了什么、传了什么、返回什么。少了 result,事后分不清是工具失败还是模型编造。
契约要能校验。JSON Schema 约束必填、类型、枚举、范围,additionalProperties: false 拒绝未知字段。解析失败要有兜底,不能让流程崩掉。
设计文档写清楚:
- 角色英文名、中文诨名、负责的子任务。
- 每个角色的输入、输出字段。
- 工具权限、失败处理、终止条件。
比如 Planner 输出 steps,Executor 输入 steps、输出 results,QA 输入 draft、输出 final。交接时不用猜。
执行过程用状态机:planned → executing → reviewing → done/failed,非法迁移直接拒绝。设最大步数、最大重试次数、总 token/时间预算;检测重复动作和无进展,触发重规划或转人工。子 Agent 失联不代表它的外部动作没发生,重试前确认幂等。
常见失败模式:
- 角色职责重叠,Planner 又规划又执行;
- 契约没 schema,Executor 收到一段自然语言;
- QA 只是把生成结果再生成一遍,没有事实核查;
- Executor 工具权限过大,能删库;
- 没有终止条件,循环烧钱;
- 错误逐级传递,最后没人负责。
题干 8.3 的文字和代码表有一处不一致:文字说三行诨名依次是“调度员、__、质检员”,代码表第三行却是 | __ | __ |。按代码表的结构,第三行是 QA,诨名应为质检员;严格按文字顺序读,空会落在第二行 Executor 的“维修工”上。这份答案按代码表作答,并在题目解析里注明这个矛盾。
代码说明
| 字段 | 类型 | 含义 |
|---|---|---|
ticket_id | string | 工单编号,串起日志和结果 |
intent | string | 意图类别,如 repair |
steps | array | 有序步骤列表 |
tool | string | 工具名,必须在注册表里存在 |
args | object | 工具参数 |
result | object | 工具返回,日志里必须记 |
JSON 例子里的 steps 是数组,执行时按顺序遍历;tool 和 args 是每一步的固定字段。json.loads 解析后就是 Python 的 dict/list,可以直接用 ["steps"]、[0]["tool"] 取值。
题目 8.1(填空题)
工单流程为“读者报障→意图解析与规划→工具执行→质检复核→输出建议”。三个角色分工如下表,请补全角色英文名与诨名;此外设计文档还须为每个角色写明输入与 __【64】__ 两类字段约定,以保证角色间 I/O 契约完整。
| 角色 | 诨名 | 负责子任务 ||---|---|---|| __【62】__ | 调度员 | 意图解析与任务规划,输出步骤列表 || Executor | __【63】__ | 按计划调用工具并执行步骤 || QA | 质检员 | 对建议做质检与合规复核,并面向用户输出 |点击查看答案
答案:
【62】:Planner【63】:维修工【64】:输出
解析:三角色按职责分工:Planner 解析意图、拆步骤;Executor 调工具;QA 做质检和合规复核。题面第一行是“意图解析与任务规划”,对应 Planner;Executor 的诨名是“维修工”;I/O 契约要写输入和输出两类字段。
题目 8.2(填空题)
角色间 I/O 约定采用 JSON 契约。请补充 intent 取值、steps 中表示工具名的字段名,以及执行日志必须记录的第三个字段名。
{"ticket_id": "T2001", "intent": "__【65】__", "steps": [{"__【66】__": "shelf_query", "args": {"room": "R305"}}]}
# Executor 每次工具调用须记录三个字段:# {"tool": "shelf_query", "args": {...}, "__【67】__": {...}}点击查看答案
答案:
【65】:repair(按题面推断)【66】:tool【67】:result
解析:工单意图是报修,取 repair;steps 里表示工具名的字段是 tool;执行日志要能复盘“调了什么、传了什么、返回什么”,第三个字段是 result。题面 JSON 骨架和日志注释分别对应这三个空。
题目 8.3(填空题)
请把三角色设计与输入/输出字段约定写入设计文档 agent_design.md。设计文档须写出三角色的英文名与分工。其中负责“对建议做质检与合规复核,并面向用户输出”的角色英文名是 __【68】__;三行对应的中文诨名依次为“调度员、__【69】__、质检员”。
# 角色设计(节选)| 角色 | 诨名 ||---|---|| Planner | 调度员 || Executor | 维修工 || __【68】__ | __【69】__ |点击查看答案
答案:
【68】:QA【69】:质检员(按题面推断)
解析:QA 的职责是质检、合规复核并面向用户输出,对应题面第三行;代码表第三行是 QA,所以诨名取“质检员”。题干文字的顺序与代码表冲突,按代码表作答。
任务 9:多工具驱动的智能体流程实现
基础知识
模型只输出工具名和参数,真正的 HTTP 请求由宿主程序执行。模型说“已执行”不算凭证。
工具注册表就是名字到 URL 的映射:
TOOLS = { "shelf_query": "http://127.0.0.1:9500/tools/shelf_query", "log_query": "http://127.0.0.1:9500/tools/log_query", "kb_query": "http://127.0.0.1:9500/tools/kb_query", "resource_query": "http://127.0.0.1:9500/tools/resource_query",}
def call_tool(name, args): return requests.get(TOOLS[name], params=args, timeout=5).json()| 环节 | 关键点 | 本题 |
|---|---|---|
| 注册 | 名字、URL、描述、schema | TOOLS 字典 |
| 调用 | GET 查询 / POST 写入 | requests.get(..., params=...) |
| 解析 | json.loads 字符串转对象 | Planner 输出 |
| 执行 | 按 steps 顺序收集 results | Executor |
| 记录 | tool、args、result | JSONL 日志 |
Function Calling 的完整链路:
- 应用把工具 schema 发给模型。
- 模型决定是否调用,返回工具名和参数。
- 应用执行工具,把结果回传给模型。
- 模型基于结果继续生成,或再调用下一个工具。
工具 schema 用 JSON Schema:
{ "type": "function", "function": { "name": "shelf_query", "description": "查询书架设备状态;不要用它查日志", "parameters": { "type": "object", "properties": { "room": {"type": "string", "description": "房间号,如 R305"} }, "required": ["room"], "additionalProperties": false }, "strict": true }}description写清用途、边界和“什么时候不要用”,它比函数名更影响选型。required标必填;additionalProperties: false拒绝多余参数;strict: true要求严格匹配 schema(支持该选项的接口)。- 工具职责不要重叠;参数尽量用枚举、绝对路径、明确单位,减少模型犯错。
- 工具结果回传时,OpenAI 用
role: "tool"+tool_call_id,Anthropic 用tool_result块 +tool_use_id;并行调用要按 id 对应。 tool_choice可控制 auto / required / none / 指定工具;不是所有模型都支持。- 工具返回同样是不可信输入,可能带提示注入;把它当数据,不要当指令。
查询用 GET,参数放 params;写操作用 POST,JSON 放 json。timeout=5 是硬约束,工具卡住时不能让智能体无限等。读操作可以重试;写操作先确认幂等,否则会重复下单、重复扣款。
Planner 的系统消息里写清 schema,用户消息放工单内容:
messages = [ {"role": "system", "content": PLANNER_SYSTEM}, {"role": "user", "content": ticket},]plan = llm_generate(messages)if isinstance(plan, str): steps = json.loads(plan)["steps"]else: steps = plan["steps"]模型经常在 JSON 外面裹代码围栏或解释文字,解析前剥离;解析失败要有兜底计划。别假设模型一定按 schema 输出。
执行阶段按 steps 顺序调工具。有依赖的步骤串行;无依赖的读操作可以并行,写操作串行或加锁。某一步失败时,决定跳过、重试、终止还是转人工,并把失败原因写进结果,不要静默返回空。
最后 qa_check(draft) 做质检:查事实、权限、合规和格式。质检不是再生成一遍,要能拿到工具返回和检索片段作为证据。
日志用 JSONL,一行一个对象,追加写:
path = "submission/m3/agent_run_log.jsonl"with open(path, "a", encoding="utf-8") as f: line = json.dumps({"tool": name, "args": args, "result": result}, ensure_ascii=False) f.write(line + "\n")json.loads 是字符串转对象,json.dumps 是对象转字符串;open(..., "w") 会清空旧日志,审计场景必须用 a。每条日志至少带时间、工具名、参数、结果、耗时和 trace_id,敏感参数脱敏。
工具顺序有语义。空调告警工单先 shelf_query 确认设备状态,再 log_query 查服务日志;先证据、后诊断。反过来先查日志,可能把历史故障当成当前状态。
几个坑:
- 没有超时;
- 重试非幂等写操作;
- 把工具返回当可信指令(提示注入);
- 日志里写密钥;
json.loads/json.dumps混用;- 工具描述太模糊导致选错;
- 没有最大轮次导致死循环。
代码说明
| 代码 | 作用 |
|---|---|
TOOLS = {...} | 名字到 URL 的注册表 |
TOOLS[name] | 按名字取 URL,名字不存在会抛 KeyError |
requests.get(url, params=args, timeout=5) | GET 请求,params 自动拼查询串 |
.json() | 响应体转 dict |
json.loads(plan) | JSON 字符串转 dict/list |
isinstance(plan, str) | 判断模型返回的是字符串还是已经解析好的对象 |
json.dumps(obj, ensure_ascii=False) | 对象转 JSON 字符串,保留中文 |
open(path, "a", encoding="utf-8") | 追加模式打开,不清空旧日志 |
f.write(line + "\n") | JSONL 一行一条,换行分隔 |
JSONL 长这样:
{"tool": "shelf_query", "args": {"room": "R305"}, "result": {"status": "空调停机"}}{"tool": "log_query", "args": {"service": "rfid"}, "result": {"log": "..."}}每行是独立 JSON,不是数组;可以逐行追加、逐行读取。json.loads 一次解析一行,json.dumps 一次生成一行。
题目 9.1(填空题)
在智能体中注册工具并封装调用。请补充工具名(URL 见附件 kb_tools_server.py)、HTTP 方法与超时秒数(本题指定 5 秒)。
TOOLS = { "shelf_query": "http://127.0.0.1:9500/tools/shelf_query", "__【70】__": "http://127.0.0.1:9500/tools/log_query", "kb_query": "http://127.0.0.1:9500/tools/kb_query", "resource_query": "http://127.0.0.1:9500/tools/resource_query"}
def call_tool(name, args): return requests.__【71】__(TOOLS[name], params=args, timeout=__【72】__).json()点击查看答案
答案:
【70】:log_query【71】:get【72】:5
解析:工具注册表用名字映射 URL;日志查询工具名是 log_query;查询类接口用 GET,参数放 params;题面指定超时 5 秒,所以填 log_query、get、5。
题目 9.2(填空题)
实现“报障→规划→工具执行→质检→建议”完整流程。请补充:Planner 约束模型只输出 JSON 所用的角色名、解析 JSON 字符串的方法名、质检复核函数名(本题指定为 qa_check)。
messages = [{"role": "__【73】__", "content": PLANNER_SYSTEM}, {"role": "user", "content": ticket}]plan = llm_generate(messages)steps = json.__【74】__(plan)["steps"] if isinstance(plan, str) else plan["steps"]results = [call_tool(s["tool"], s["args"]) for s in steps]draft = executor_summarize(steps, results)final = __【75】__(draft)点击查看答案
答案:
【73】:system【74】:loads【75】:qa_check
解析:Planner 的“只输出 JSON”是系统级约束,放 system;模型返回字符串时用 json.loads 转对象;质检函数按题面指定为 qa_check。所以三个空是 system、loads、qa_check。
题目 9.3(填空题)
空调告警工单的规划顺序为“先确认设备状态、后查询服务日志”。第二步查询日志的工具为 log_query(URL 见题目 9.1),则第一步确认设备状态的工具是 __【76】__(工具名)。
# 规划顺序:先确认,后查日志steps = [{"tool": "__【76】__", "args": {"room": "R305"}}, {"tool": "log_query", "args": {"service": "rfid"}}]点击查看答案
答案: shelf_query
解析:题面要求先确认设备状态、后查服务日志。设备状态对应 shelf_query,日志对应 log_query;顺序反了会把历史日志当成当前状态,所以第一空填 shelf_query。
题目 9.4(填空题)
运行日志采用追加写、逐行记录。请补充打开文件时的模式参数(单字符),以及把字典序列化为 JSON 字符串的方法名(注意与题目 9.2 的解析方法区分)。
f = open("submission/m3/agent_run_log.jsonl", "__【77】__", encoding="utf-8")line = json.__【78】__({"tool": name, "args": args, "result": result}, ensure_ascii=False) + "\n"f.write(line)点击查看答案
答案:
【77】:a【78】:dumps
解析:题面要求日志追加写、逐行记录。JSONL 一行一个 JSON,所以模式是 a;对象转 JSON 字符串用 json.dumps,ensure_ascii=False 保留中文。解析用 json.loads,不要混。
题目 9.5(填空题)
请阅读附件 kb_tools_server.py 源码回答问题(答案以附件为准)。
# 附件源码节选def log_query(service: str = Query("__【79】__")): # 1) 默认服务代号 return {"service": service, "log": "..."}
def shelf_query(room: str = Query("R305")): return {"room": room, "status": "空调停机", "level": __【80】__} # 2) 告警级别
def kb_query(q: str = Query("借阅卡补办")): return {"q": q, "result": "命中:...维修/制卡人员2个工作日内__【81】__处理。"} # 3) 处置词
@app.get("/tools/__【82】__") # 4) 第 4 个工具(电子资源并发状态查询)def __【82】__(db: str = Query("cnki")): return {"db": db, "concurrent": 128, "limit": 120, "status": "已满"}点击查看答案
答案:
【79】:rfid(按题面推断)【80】:2(按题面推断)【81】:完成(按题面推断)【82】:resource_query
解析:第 4 个工具是电子资源并发状态查询,路由和函数名都是 resource_query;默认服务代号取 9.3 示例的 rfid;停机告警按三级制取 2;处置词取“完成”。79–81 依赖附件,实际以 kb_tools_server.py 为准。
任务 10:跨场景编排与安全管控
基础知识
路由决定“这句话该谁答”。关键词规则最简单:
REPAIR_KEYWORDS = ["报修", "故障", "空调", "门禁", "网络"]
def route(question: str) -> str: if any(k in question for k in REPAIR_KEYWORDS): return "repair_agent" return "visitor_agent"| 方式 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 关键词/规则 | 快、便宜、可解释 | 同义词、错别字会漏 | 领域词稳定 |
| 语义相似 | 能处理同义表达 | 需要标注和阈值 | 意图边界清晰 |
| 模型分类 | 能利用上下文 | 多一次调用,可能不稳定 | 复杂意图 |
| 混合路由 | 规则兜底 + 模型补充 | 逻辑复杂 | 生产常用 |
统一入口的返回体要带 agent,值是 repair_agent 或 visitor_agent。没有这个字段,前端分不清场景,日志也没法按智能体统计。再带上 trace_id、耗时、是否降级和引用证据。
敏感指令在模型和工具之前拦:
SENSITIVE = ["删除数据库", "关机", "重启服务器", "清除日志"]
if any(s in question for s in SENSITIVE): return {"reply": "该操作涉及敏感指令,已拒绝执行。", "blocked": True}这是输入侧护栏。黑名单只能挡已知表达,换同义词、拼音、拆字、编码就可能绕过;要配合最小权限、工具白名单、参数校验、人工确认和审计。输入先做规范化:全角转半角、去零宽字符、统一大小写、还原常见编码。
权限校验是授权,不是认证。认证回答“你是谁”,授权回答“你能做什么”。
role = request.headers.get("X-User-Role", "user")if action in ["restart", "close_ticket"] and role != "admin": raise HTTPException(status_code=403, detail="权限不足")| 状态码 | 含义 | 场景 |
|---|---|---|
| 401 | 未认证 | 没带 token、token 过期 |
| 403 | 已认证但无权限 | 普通用户执行管理员操作 |
| 404 | 资源不存在 | 工单 ID 错误 |
| 429 | 限流 | 请求过多 |
| 500 | 服务端错误 | 未捕获异常 |
| 503 | 服务不可用 | 维护、熔断 |
生产环境不要直接信任客户端传来的 X-User-Role,由网关注入或签名校验。角色校验放在真正的执行入口,不要只在提示词里写“你是管理员”。
审计用 JSONL:
record = { "timestamp": now_iso(), "user": user, "action": action, "result": result,}append_jsonl(AUDIT_LOG_PATH, record)追加写,只追加不修改;拦截事件也要记,否则只能查到成功操作。日志防篡改、设保留期,不要把密码和 token 写进去。完整一点还要带 trace_id、资源 ID、参数摘要、模型版本、工具版本。
安全做纵深防御:输入过滤 → 意图/权限校验 → 工具白名单 → 沙箱/最小权限 → 输出过滤 → 审计监控。任何一层都不是绝对安全;系统提示不是安全边界,模型可以被诱导,真正的边界在代码和权限系统里。
容易出错的地方:直接信任 X-User-Role;只拦“删除数据库”不拦同义表达;只记录成功操作;审计日志普通用户能删;工具权限过大;没有限流;把模型输出直接当命令执行。
代码说明
| 代码 | 作用 |
|---|---|
any(k in question for k in REPAIR_KEYWORDS) | 任一关键词出现在问题里就返回 True |
question | 用户输入字符串 |
request.headers.get("X-User-Role", "user") | 取请求头,缺失时默认 user |
raise HTTPException(status_code=403, detail=...) | 抛 403,FastAPI 转成 JSON 错误响应 |
now_iso() | 当前时间的 ISO 字符串 |
append_jsonl(path, record) | 把一条记录追加到 JSONL |
any(...) 里是生成器表达式,命中第一个关键词就短路,不会继续遍历。headers.get 的第二个参数是默认值,没传角色头时按普通用户处理。
题目 10.1(填空题)
基于关键词的规则路由:命中运维/设施关键词进“设备报修智能体”(本题指定标识符 repair_agent),否则进“访客咨询智能体”(本题指定标识符 visitor_agent)。请补充标识符、路由方式(四个中文,见题面括号提示)与统一入口用于标明实际应答者的字段名(本题指定)。
REPAIR_KEYWORDS = ["报修", "故障", "空调", "门禁", "网络"]
def route(question: str) -> str: if any(k in question for k in REPAIR_KEYWORDS): return "__【83】__" return "__【84】__"
# 该路由方式属于 __【85】__ 路由# 统一入口返回结果中用 __【86】__ 字段标明实际应答的智能体点击查看答案
答案:
【83】:repair_agent【84】:visitor_agent【85】:基于规则(按题面推断)【86】:agent(按题面推断)
解析:关键词路由命中“报修/故障/空调/门禁/网络”就交给设备报修智能体,否则交给访客咨询智能体。题面指定标识符 repair_agent / visitor_agent,所以前两空是它们;这是基于规则的路由;返回体用 agent 标明实际应答者。
题目 10.2(填空题)
敏感指令拦截。请补充拦截文案、标记字段的值,以及该机制所属的安全防护层级(两字中文)。
SENSITIVE = ["删除数据库", "关机", "重启服务器", "清除日志"]
if any(s in question for s in SENSITIVE): return {"reply": "__【87】__", "blocked": __【88】__}# 该机制属于 __【89】__ 安全防护(拦截事件须写入审计日志)点击查看答案
答案:
【87】:该操作涉及敏感指令,已拒绝执行。(按题面推断)【88】:True【89】:输入(按题面推断)
解析:敏感指令拦截是输入侧护栏,命中高风险动作就拒绝;题面要求固定文案并标记 blocked,所以文案可用“该操作涉及敏感指令,已拒绝执行。”,blocked=True;机制属于输入安全,拦截事件要写审计日志。
题目 10.3(填空题)
权限校验:重启设备、关闭工单为高权限操作,仅管理员可执行。请补充角色名、无权限时的状态码,以及该校验所属的控制类型(两字中文)。
role = request.headers.get("X-User-Role", "user")if action in ["restart", "close_ticket"] and role != "__【90】__": raise HTTPException(status_code=__【91】__, detail="权限不足")# 该校验属于 __【92】__ 控制点击查看答案
答案:
【90】:admin【91】:403【92】:权限
解析:认证回答“你是谁”,授权回答“你能做什么”。重启设备、关闭工单是高权限动作,题面要求仅管理员可执行,所以角色是 admin;身份已知但权限不足返回 403;这是权限控制。
题目 10.4(填空题)
操作审计采用 JSONL 格式,每条记录含四个字段(本题指定字段集为 timestamp、user、action、result)。其中记录“操作发生时间”的字段名是 __【93】__;记录“操作者身份”的字段名是 __【94】__。
record = {"__【93】__": now_iso(), "__【94】__": user, "action": action, "result": result}append_jsonl(AUDIT_LOG_PATH, record)点击查看答案
答案:
【93】:timestamp【94】:user
解析:审计记录要能回答“谁、何时、做了什么、结果如何”。题面指定字段集 timestamp、user、action、result,所以操作时间是 timestamp,操作者身份是 user。
考前检查清单
- 代码填空只写空缺本身,不带等号、引号、括号或分号。
json.loads解析字符串,json.dumps序列化对象;JSONL 用追加模式a。- 查询类工具用
requests.get(..., params=...);对话、摘要类接口用requests.post(..., json=...)。 max_new_tokens数的是 token,不是汉字;字符数用len()。- 401 是未认证,403 是已认证但无权限;高权限动作只给
admin。 - FAISS 用内积做余弦相似度时,文档向量和查询向量都要 L2 归一化。
- 缓存要设 TTL;降级响应保持 HTTP 200,并用
fallback: true标记。 - 审计日志只追加不修改,拦截事件也要记录。