面经
约 6318 字大约 21 分钟
2026-06-02
字节
Q2:开发 Agent 的时候,用的是什么流程?为什么用这个流程? 答: 我用的是"先单点验证,再图编排"的流程。
第一步,先独立验证每个工具能正常工作。比如 MCP 的日志查询工具、Prometheus 告警工具,单独写测试脚本确认输入输出正确。
第二步,用 LangChain 的 @tool 把工具封装成标准接口,用简单的 ReAct Agent 测试工具调用链路是否通。
第三步,用 LangGraph 的 StateGraph 把多个节点编排成工作流。运维 Agent 的 Planner → Executor → Replanner 就是这样一步步搭起来的。
为什么不用"先设计完整架构再开发"?因为 Agent 开发最大的不确定性在 LLM 的行为——prompt 写得不好,LLM 可能不调工具、调错工具、输出格式不对。如果先画完整架构再开发,到最后发现 LLM 不按预期工作,改动成本很大。先单点验证再组装,每一步都能快速试错迭代。
Q3:开发 Agent 的时候,有没有借鉴目前各个产品的思路? 答: 借鉴了三个产品。
ChatGPT 的 Function Calling 模式:LLM 不直接执行操作,而是输出结构化的工具调用请求,由外部执行。我的 Chat Agent 就是这个模式,LLM 输出 tool_calls,ToolNode 负责执行。
Cursor / Claude Code 的 Plan-Execute 模式:复杂任务先生成计划再逐步执行。我的 AIOps Agent 借鉴了这个思路,Planner 先制定诊断步骤,Executor 逐步执行,Replanner 动态调整。
Dify / Coze 的工作流编排:把 Agent 拆成节点,用图连接。我用 LangGraph 的 StateGraph 实现了类似的效果,每个节点职责单一,可以独立调试和替换。
另外 MCP 协议本身就是 Anthropic 提出的标准,我直接采用了它的工具解耦思想,本地开发用 mock 服务,生产切换到真实服务。
Q4:模型和 Agent 的区别是什么? 答: 模型是"大脑",Agent 是"有手有脚的大脑"。
模型只能做一件事:输入文本,输出文本。它不能上网、不能查数据库、不能执行命令。你问它"现在几点",它只能猜。
Agent 在模型基础上加了三样东西:
工具:能调用外部能力(查日志、查监控、检索知识库) 记忆:能记住之前的对话(MemorySaver) 规划:能决定下一步做什么(ReAct 循环或 Plan-Execute 工作流) 用我项目举例:ChatQwen 是模型,只能聊天。加上 @tool、MemorySaver、LangGraph 编排之后,变成了 Agent——用户问"最近有什么告警",它能自己决定调用 Prometheus 查询工具,拿到结果后组织语言回答。
一句话总结:模型是被动回答,Agent 是主动行动。
Q5:本地工具、MCP Tools、Skill 三者的区别? 答: 三者是不同层级的概念。
本地工具是写死在代码里的函数,用 @tool 装饰器定义,和 Agent 进程同生命周期。优点是调用快、无网络开销,缺点是和代码耦合,改工具要改代码重新部署。我项目中的 retrieve_knowledge、get_current_time 就是本地工具。
MCP Tools 是通过 MCP 协议调用的远程工具,工具作为独立服务运行,Agent 通过 HTTP/SSE 调用。优点是解耦——工具可以独立开发、独立部署、多 Agent 共享,缺点是多了网络开销和协议开销。我项目的 CLS 日志查询和 Monitor 监控就是 MCP Tools。
Skill 是更高层的抽象,不只是一个工具函数,而是一套完整的"能力包"——包含提示词模板、工具组合、执行脚本、配置文件。一个 Skill 可以编排多个工具完成一个复合任务。比如"代码审查 Skill"内部可能调用 grep 工具搜索代码、用 LLM 分析问题、用 edit 工具修改文件,对外暴露一个"审查代码"的能力。
本地工具 MCP Tools Skill 粒度 单个函数 单个远程服务 能力包(多工具+提示词+脚本) 耦合度 和代码紧耦合 通过协议解耦 完全独立,可插拔 适用场景 简单、高频、低延迟 跨系统、可复用 复合任务、可共享 Q6:为什么已经有了 MCP,Anthropic 不继续做 MCP 而是转向做 Skill 了? 答: 因为 MCP 解决的是"工具怎么调"的问题,但没解决"能力怎么组合"的问题。
MCP 定义了工具的通信协议——Agent 怎么发现工具、怎么传参、怎么拿结果。但实际使用中,一个复杂任务往往需要编排多个工具、配合特定的提示词、遵循特定的执行流程。这些编排逻辑散落在 Agent 代码里,无法复用。
Skill 就是把这部分编排逻辑也标准化了。一个 Skill 包含:配置文件(声明依赖的工具和参数)、提示词模板(告诉 LLM 怎么用这些工具)、执行脚本(具体的编排逻辑)。这样"代码审查"这个能力可以打包成一个 Skill,任何人拿到就能用,不需要自己写编排代码。
类比的话:MCP 是 USB 接口标准,Skill 是 USB 设备。有了接口标准还不够,还需要有具体的设备(鼠标、键盘、U 盘)才能完成任务。MCP 是 Skill 的基础设施,不是替代关系。
Q7:Skill 里面有没有工具? 答: 有。Skill 是上层编排,工具是底层能力,Skill 内部通过调用工具来完成任务。
一个 Skill 的典型结构:
.agents/skills/code-review/
├── .yaml # 配置:声明依赖哪些工具
└── scripts/
└── review.js # 执行脚本:调用工具的编排逻辑yaml 配置中声明这个 Skill 需要用到 grep 工具、read 工具、edit 工具。执行脚本中按顺序调用这些工具:先 grep 搜索相关代码,再 read 读取文件内容,再用 LLM 分析问题,最后用 edit 修改代码。
Skill 本身不实现工具,它引用和编排已有的工具。就像一个函数调用其他函数一样,Skill 是"函数的函数"。
Q8:Claude Code 的记忆架构是什么?上下文真的等于记忆吗? 答: 上下文不等于记忆,两者是不同层次的概念。
上下文是模型单次推理时能看到的信息,受 context window 限制。Claude Code 的上下文包括:系统提示、对话历史、工具调用结果、文件内容等。上下文是暂时的,这次对话结束就没了。
记忆是跨会话持久化的信息。Claude Code 的记忆分几层:
会话内记忆:当前对话的上下文,靠 context window 项目记忆:.claude/settings.json 等配置文件,记录项目偏好 全局记忆:CLAUDE.md 文件,记录用户的编码习惯、常用命令等 类比人脑:上下文是"工作记忆"(你正在想的事),记忆是"长期记忆"(你过去学到的东西)。工作记忆容量小但快,长期记忆容量大但需要"检索"才能调用。
我项目中的 MemorySaver 也类似:当前对话的 messages 列表是上下文,MemorySaver 持久化的是记忆。对话修剪策略就是控制上下文大小,但完整历史存在记忆中可以回溯。
Q9:为什么 Claude Code 不用 RAG 检索代码而是直接用 grep? 答: 因为代码检索的需求和自然语言文档检索不同。
RAG 适合的场景是语义模糊的查询——用户问"怎么处理 CUDA 报错",需要理解语义相似性。但代码检索通常是精确查询——"找所有调用 connect() 的地方"、"找到定义 UserService 的文件"。这种场景 grep 的正则匹配比向量检索更快更准。
而且代码有结构化特征(函数名、类名、import 路径),grep 能精确匹配这些符号。RAG 的 embedding 会把代码转成语义向量,反而丢失了符号级别的精确性。比如搜索 def connect,grep 精确匹配,RAG 可能把 connect_database 和 connect_to_redis 都检索出来。
另外 grep 是零成本的,不需要 embedding 模型、不需要向量数据库。Claude Code 作为开发工具,必须轻量快速,启动一个 Milvus 就为了检索代码不合理。
但 RAG 在代码场景也有用武之地——比如搜索"和用户认证相关的代码",这种语义查询 grep 做不到,需要向量检索。所以两者是互补的,不是替代的。
Q10:有没有了解过最前沿的记忆设计? 答: 了解过几个方向。
Mem0(MemoryOS):给 Agent 设计的分层记忆系统,分三层——语义记忆(知识和事实)、情景记忆(过去的交互历史)、程序记忆(学会的行为模式)。它会自动从对话中提取关键信息存入记忆库,下次对话时按相关性检索。解决了"Agent 每次对话都从零开始"的问题。
Generative Agents(斯坦福小镇):25 个 AI Agent 在虚拟小镇生活,每个 Agent 有记忆流(Memory Stream)、反思机制(Reflection)和规划能力(Planning)。记忆流记录所有事件,反思定期从记忆中提取高级洞察,规划时检索相关记忆决定下一步行动。关键是"反思"机制——不是简单存储原始记忆,而是主动抽象和总结。
Reflexion:Agent 执行任务后,把失败经验总结成自然语言存入记忆,下次遇到类似任务时检索这些经验避免重复犯错。类似人类的"复盘"。
我自己项目的改进方向:当前 MemorySaver 只是简单存储对话历史,后续可以借鉴 Mem0 的思路,从对话中自动提取关键信息(比如用户提到的服务器配置、历史故障)存入结构化记忆库,下次对话时检索相关记忆注入上下文,而不是存储和检索原始对话文本。
Q11:手撕:实现一个 Skill 系统 思路: 三个核心模块——注册、发现、调用。
目录结构:
.agents/skills/ ├── code-review/ │ ├── .yaml │ └── scripts/ │ └── review.js ├── log-analyzer/ │ ├── .yaml │ └── scripts/ │ └── analyze.js yaml 配置文件:
name: code-review description: 审查代码质量和安全问题 version: 1.0.0 tools_required: [grep, read, edit] parameters:
- name: file_path type: string required: true description: 要审查的文件路径 entry_point: scripts/review.js 核心实现思路:
import os, yaml, importlib, subprocess
class SkillRegistry:
def __init__(self, skills_dir=".agents/skills"):
self.skills_dir = skills_dir
self.skills = {} # name -> skill_config
def discover(self):
"""扫描目录,发现所有 Skill"""
for name in os.listdir(self.skills_dir):
yaml_path = os.path.join(self.skills_dir, name, ".yaml")
if os.path.exists(yaml_path):
with open(yaml_path) as f:
config = yaml.safe_load(f)
config["_dir"] = os.path.join(self.skills_dir, name)
self.skills[config["name"]] = config
def register(self, name, config):
"""手动注册 Skill"""
self.skills[name] = config
def list_skills(self):
"""列出所有可用 Skill"""
return [
{"name": s["name"], "description": s["description"]}
for s in self.skills.values()
]
async def invoke(self, skill_name, **kwargs):
"""调用 Skill"""
skill = self.skills.get(skill_name)
if not skill:
raise ValueError(f"Skill not found: {skill_name}")
# 1. 校验参数
for param in skill.get("parameters", []):
if param["required"] and param["name"] not in kwargs:
raise ValueError(f"Missing required param: {param['name']}")
# 2. 检查依赖工具是否可用
for tool_name in skill.get("tools_required", []):
if not self._check_tool(tool_name):
raise RuntimeError(f"Tool not available: {tool_name}")
# 3. 执行 Skill 脚本
entry = os.path.join(skill["_dir"], skill["entry_point"])
result = await self._run_script(entry, kwargs)
return result
async def _run_script(self, script_path, params):
"""执行 Skill 脚本,传入参数"""
import json
proc = await asyncio.create_subprocess_exec(
"node", script_path,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
)
stdout, _ = await proc.communicate(json.dumps(params).encode())
return json.loads(stdout.decode())
def _check_tool(self, tool_name):
"""检查工具是否已注册"""
return tool_name in self.available_tools关键设计点:
发现机制:扫描 .agents/skills/ 目录,解析 yaml 配置自动注册,不需要手动 import 参数校验:yaml 中声明参数类型和必填项,调用时自动校验 工具依赖检查:Skill 声明依赖哪些工具,调用前检查工具是否可用 隔离执行:Skill 脚本在子进程中执行,通过 stdin/stdout 传参和返回结果,和主进程隔离,一个 Skill 崩溃不影响其他 和我项目的关联:我项目的 @tool 是单个工具函数,MCP 是远程工具服务,如果要做 Skill,就是在这之上再包一层——把"检索知识库 + 查询监控 + 生成报告"的编排逻辑打包成一个 AIOps Skill。
宇树科技
Q2:解释一下什么是向量数据库,它与传统关系型数据库的核心区别是什么? 答: 向量数据库是专门存储和检索高维向量的数据库。它把文本、图片等非结构化数据通过 embedding 模型转换成一组浮点数(比如 1024 维的向量),存储起来后支持按向量相似度搜索。
核心区别有三个:
数据模型不同:关系型数据库存结构化数据(行和列),查询靠精确匹配 SQL 的 WHERE name = 'xxx'。向量数据库存高维向量,查询靠相似度计算——"找出和这个向量最像的 Top-K 个"。
查询语义不同:关系型数据库是精确查询,要么匹配要么不匹配。向量数据库是模糊查询,返回的是"相似度最高"的结果,没有绝对的对错。
索引方式不同:关系型用 B-Tree 或 Hash 索引。向量数据库用 ANN(近似最近邻)算法,比如 HNSW、IVF,在精度和速度之间做 trade-off,不能保证找到绝对最近的,但能保证很快。
我项目中用 Milvus 存储运维文档的向量,用户提问时把问题也向量化,做余弦相似度搜索取 Top-3。这个场景用关系型数据库做不了,因为"怎么处理 CUDA 报错"和"GPU 驱动版本不兼容的解决方案"关键词完全不同,但语义相似,只有向量检索能匹配上。
Q3:在 RAG 流程中,如果检索到的文档相关性不高,导致生成答案质量差,可能有哪些原因?如何优化? 答: 从检索链路逐层分析原因和优化方案。
原因一:分块策略不合理。 chunk_size 太大,一个块里混了多个主题,embedding 被稀释;太小,上下文断裂,语义不完整。
优化:我项目用了两阶段分割——先按 Markdown 标题切保证语义边界,再按字符数细分控制大小。针对运维文档测试了 800/1200/1600/2000 四组参数,1600 效果最好。
原因二:embedding 模型表达能力不够。 模型不能很好地理解领域术语,"OOM"和"内存溢出"在向量空间中距离远。
优化:可以 fine-tune embedding 模型,或者在文档中补充同义词("OOM / Out of Memory / 内存溢出")。
原因三:查询和文档的语义 gap。 用户问"训练卡住了",文档写的是"GPU 显存溢出导致进程挂起",字面差异大但语义相关。
优化:Query Rewriting,在检索前用 LLM 改写用户问题。比如把"训练卡住了"改写成"GPU 训练进程挂起 显存 OOM",再去做向量检索,召回率会提升。
原因四:TopK 设置不当。 TopK 太小遗漏关键信息,太大引入噪声。
优化:我项目测试了 TopK 3/5/10,运维文档信息密度高,TopK=3 在召回和噪声之间平衡最好。也可以用 Re-ranking——先取 Top-20,再用交叉编码器重新排序取 Top-3。
原因五:元数据没有利用。 纯向量检索忽略了文档结构信息。
优化:我项目在分块时保留了标题元数据(h1、h2),检索时可以先按标题过滤再做语义匹配。比如用户问 CUDA 相关问题,先过滤标题含"CUDA"的文档块,再做相似度搜索。
Q4:谈一谈你对 ReAct 框架的理解。它的核心思想是什么,如何帮助 Agent 进行推理和行动? 答: ReAct 的核心思想是把推理(Reasoning)和行动(Acting)交替进行,形成一个循环。
传统方式要么纯推理(Chain-of-Thought,LLM 只想不做),要么纯行动(直接调工具,不思考为什么调)。ReAct 把两者结合:LLM 先想(Thought)→ 决定做什么(Action)→ 执行并观察结果(Observation)→ 继续想。
具体循环:
用户: "最近有什么告警?" ↓ Thought: 用户想知道告警信息,我需要调用 Prometheus 告警查询工具 Action: query_prometheus_alerts() Observation: 发现 2 条活跃告警:CPU 使用率 92%、内存使用率 85% ↓ Thought: 用户可能还想了解详情,我再查一下相关日志 Action: search_log(service_name="data-sync-service") Observation: 发现大量 ConnectionTimeout 错误 ↓ Thought: 信息足够了,综合告警和日志给用户一个完整的回答 Answer: "检测到 2 条告警..." ReAct 帮助 Agent 的关键在于每一步都有明确的推理过程。LLM 不是随机调工具,而是先解释为什么要调这个工具,再调用,拿到结果后再推理下一步。这让 Agent 的行为可解释、可调试。
我项目中 Chat Agent 用 LangChain 的 create_agent 实现 ReAct,内部自动管理 Thought-Action-Observation 循环,不需要手动拼 prompt。
Q5:在 AI Agent 中,记忆(Memory)通常分为哪几种类型?简要说明它们的作用。 答: 通常分为三种类型。
短期记忆(Short-term Memory):当前对话的消息列表。用户说了什么、Agent 回了什么、调了哪些工具、返回了什么结果,都在这里。受上下文窗口限制,太长需要修剪或压缩。我项目中 rag_agent_service 的 messages 列表就是短期记忆,trim_messages_middleware 负责控制大小。
长期记忆(Long-term Memory):跨会话持久化的信息。用户的历史偏好、过去的交互摘要、学到的知识。我项目中 MemorySaver 存储的就是长期记忆,按 thread_id 隔离,对话历史持久化在内存中(生产环境应换 Redis/PostgreSQL)。
工作记忆(Working Memory):Agent 当前任务的中间状态。比如 Plan-Execute 模式中,当前的计划列表、已执行步骤的结果、Replanner 的评估结论。我项目中 PlanExecuteState 的 plan、past_steps、response 就是工作记忆,随图的执行流转更新。
类型 生命周期 存储位置 我项目的实现 短期记忆 单次对话 context window messages 列表 长期记忆 跨会话 持久化存储 MemorySaver 工作记忆 单次任务 图状态 PlanExecuteState Q6:实现一个简单的多轮对话状态管理,你会考虑哪些关键要素? 答: 六个关键要素。
会话标识:每个对话需要唯一 ID(session_id),用于隔离不同用户的对话。我项目用前端生成的 UUID 作为 session_id,也作为 LangGraph 的 thread_id。
消息存储:按 session_id 存储消息列表,每条消息包含 role(user/assistant)、content、timestamp。我项目用 MemorySaver 的 checkpointer 存储,按 thread_id 索引。
上下文窗口控制:消息越来越多会超出 token 限制。需要修剪策略——保留最近 N 轮或按 token 数截断。我项目保留系统消息 + 最近 6 条消息。
上下文注入:每次新对话需要加载历史上下文。有两种方式:全量加载(把所有历史塞进 prompt)和摘要加载(把历史压缩成摘要)。我项目当前是全量加载 + 修剪,后续可以加摘要机制。
会话过期清理:长时间不用的会话需要清理,防止内存泄漏。可以设置 TTL,比如 24 小时没活动自动清除。
并发隔离:多个用户同时对话,状态不能混淆。我项目每个 session_id 对应独立的 checkpointer 实例,天然隔离。
Q7:如何评估一个 AI Agent 的好坏?除了准确率,还可以关注哪些指标? 答: Agent 的评估比传统模型复杂,因为它涉及多步决策和工具调用。我从五个维度评估。
任务完成率:给定一批任务,Agent 能成功完成多少。这是最核心的指标。比如给 20 个运维问题,Agent 能正确回答 17 个,完成率 85%。
工具调用准确率:Agent 是否选择了正确的工具、传入了正确的参数。可以用"工具匹配率"(调对了哪些工具)和"参数准确率"(参数是否正确)两个子指标衡量。
推理效率:完成任务用了多少步、消耗了多少 token。ReAct 模式下,同样的问题 A 方案调了 3 次工具完成,B 方案调了 8 次,A 更高效。效率直接影响成本和延迟。
鲁棒性:面对异常输入(模糊问题、工具失败、超时)的表现。好的 Agent 能优雅降级——工具失败时告诉用户"查询超时,建议稍后重试",而不是崩溃或编造答案。
用户满意度:最终还是要看用户体验。可以通过用户反馈(点赞/踩)、对话轮数(几轮解决问题)、留存率来衡量。
我项目主要评估了任务完成率和工具调用准确率,用 20 个运维问答对测试。推理效率通过限制 max_iterations(最多 8 步)来兜底。
Q8:了解 LangChain 或 LlamaIndex 这类框架吗?谈谈它们解决了什么问题,以及可能的局限性。 答: 两个框架解决的问题不同。
LangChain 解决的是 Agent 开发的"胶水代码"问题。LLM 应用开发涉及大量重复工作——对接不同 LLM、管理对话历史、定义工具、编排工作流。LangChain 提供标准化接口(ChatModel、Memory、Tool、Chain),把这些胶水代码抽象掉。LangGraph 在此基础上提供图编排能力。我项目深度依赖 LangChain + LangGraph。
LlamaIndex 解决的是 RAG 场景的"数据接入"问题。它专注于把各种数据源(PDF、Notion、Slack、数据库)接入 LLM,提供丰富的 Loader、Index 和 Retriever。如果你的核心需求是知识库问答,LlamaIndex 比 LangChain 更专注。
局限性:
抽象层太厚,调试困难。LangChain 的报错堆栈经常十几层,定位问题很痛苦。我项目中遇到的 DashScope API Base 问题,很大原因是 LangChain 的抽象层把环境变量传递链路掩盖了。
版本迭代太快,API 不稳定。0.1 到 0.2 大量 breaking change,社区教程经常过时。
过度封装。有些场景直接调 OpenAI SDK 三行代码搞定,用 LangChain 反而要学一堆概念。简单场景不建议用。
性能开销。每层抽象都有额外开销,对延迟敏感的场景需要注意。我项目的 embedding service 就没有走 LangChain,直接用 OpenAI SDK 调 DashScope,减少一层抽象。
Q9:在开发 Agent 时,如何设计提示词(Prompt)来提升其任务执行的稳定性和准确性? 答: 我总结了五个设计原则。
角色定义清晰。系统提示第一句就告诉 LLM 它是谁、负责什么。我项目的 AIOps Planner 提示词开头是"你是一个智能运维规划专家,负责根据告警信息制定诊断计划",明确角色后 LLM 的输出风格和决策方向更稳定。
输出格式约束。要求 LLM 输出 JSON 或特定格式,用 Pydantic 模型做校验。我项目的 Planner 要求输出 {"steps": ["步骤1", "步骤2", ...]},Replanner 用 BaseModel 定义输出结构。不约束格式的话,LLM 可能输出自然语言,后续解析会失败。
工具使用指引。不要假设 LLM 会正确使用工具,在 prompt 中明确说明每个工具的用途、适用场景和参数含义。我项目的 Executor 提示词写了"如果已经指定了工具,则使用指定的工具",避免 LLM 自作主张跳过工具。
防幻觉约束。明确告诉 LLM 不要编造数据。我项目写了"不要编造数据,只返回实际获取的信息"、"所有内容必须基于工具查询的真实数据,严禁编造"。运维场景如果编造数据会误导排查方向,后果严重。
兜底逻辑。告诉 LLM 遇到异常时怎么处理。"如果工具调用失败,请说明失败原因"、"如果某个步骤失败,在结论中如实说明,不要跳过"。这样 Agent 在部分工具不可用时也能给出有用的回答。
Q10:手写算法:实现一个函数,计算两个向量的余弦相似度。 答:
import math
def cosine_similarity(vec_a: list[float], vec_b: list[float]) -> float: """计算两个向量的余弦相似度
公式: cos(θ) = (A·B) / (||A|| × ||B||)
范围: [-1, 1],越接近 1 越相似
"""
if len(vec_a) != len(vec_b):
raise ValueError("向量维度不一致")
dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
norm_a = math.sqrt(sum(a * a for a in vec_a))
norm_b = math.sqrt(sum(b * b for b in vec_b))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
验证:
完全相同 → 1.0
assert cosine_similarity([1, 0, 0], [1, 0, 0]) == 1.0
完全相反 → -1.0
assert cosine_similarity([1, 0, 0], [-1, 0, 0]) == -1.0
正交 → 0.0
assert cosine_similarity([1, 0, 0], [0, 1, 0]) == 0.0
部分相似
result = cosine_similarity([1, 2, 3], [1, 2, 4]) assert 0.99 < result < 1.0 # 非常相似但不完全相同 和项目的关联:我项目中 Milvus 默认用余弦相似度做向量搜索。用户提问 "最近有什么告警" 被 embedding 成 1024 维向量,和知识库中所有文档向量计算余弦相似度,返回 Top-3 最相似的文档片段作为上下文。
