跳转至

AI Agent精选八股文

1 工具设计原理

OpenAI Function Calling 的工作原理与 JSON Schema 设计

🔷 Function Calling 是什么?解决了什么问题?

Function Calling 是 LLM 与外部世界交互的“翻译官”和“调度员”。

它允许模型在生成自然语言之外,输出一个结构化的函数调用请求——包含函数名和参数 JSON,然后由你的应用代码去实际执行,并将结果反馈给模型。

image.png

它解决的核心问题:模型“能说不能做”的边界突破。

  • 打破信息孤岛:训练数据有截止日期,模型不知道今天的天气、股市、你的日程。Function Calling 让模型能调用实时数据。

  • 从“建议”到“执行”:模型不再只能说“你可以这样订机票”,而是直接输出 {"function": "book_flight", "parameters": {"from": "PEK", "to": "SHA", "date": "2026-07-05"}},应用代码拿过去就能自动执行。

  • 标准化输出:比起靠正则去抓取自由文本中的意图,Function Calling 给出的是一份结构化的、可直接序列化的 JSON,可靠性和开发效率大幅提升。

  • 多工具协同:Agent 需要同时操作多个工具(查天气、算公式、发邮件),Function Calling 让模型可以根据用户意图动态选择调用哪个工具、传什么参数,实现自动化的工具编排。

本质上,Function Calling 把 LLM 从一个“对话引擎”升级为一个“意图理解与任务分发引擎”。


OpenAI Function Calling 的完整工作原理与 JSON Schema 设计

2.1 工作原理

OpenAI Function Calling 的核心流程分为 定义 → 触发 → 执行 → 反馈 四个环节。

1. 定义工具
   开发者: 在 API 请求的 tools 参数中,用 JSON Schema 描述所有可用的函数。
2. 模型决策
   LLM: 分析用户输入,决定是否需要调用工具、调用哪个、参数是什么。
   若需要调用,返回 finish_reason=tool_calls,并给出函数名和参数 JSON。
3. 应用执行
   你的代码: 解析模型返回的工具调用指令,用这些参数去真正执行函数(调数据库、天气 API 等)。
4. 结果反馈
   将执行结果作为 tool 角色消息追加到 messages 列表,再次请求模型生成最终答案。

代码示例:一个完整的天气查询 + 穿衣建议对话

import openai, json

client = openai.OpenAI()

# 1. 定义工具:用 JSON Schema 描述函数
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取指定城市的当前天气,包括温度、湿度和天气状况",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,如 Beijing"
                }
            },
            "required": ["city"]
        }
    }
}]

messages = [{"role": "user", "content": "北京今天天气怎么样?我该穿什么衣服?"}]

# 2. 第一次请求:模型决定是否调用工具
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

msg = response.choices[0].message

# 3. 检查是否有工具调用
if msg.tool_calls:
    tool_call = msg.tool_calls[0]
    func_name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    # 4. 执行真正的函数
    weather_data = get_real_weather(args["city"])  # 你的业务逻辑

    # 5. 将模型调用 + 函数结果追加到消息历史
    messages.append(msg)  # 模型的工具调用消息
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(weather_data)
    })

    # 6. 第二次请求:模型根据工具结果生成最终回答
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages
    )
    print(final_response.choices[0].message.content)

关键细节:

  • tool_choice="auto" 让模型自主决策要不要调工具;"none" 强制不调用;"required" 强制调用;也可以直接指定某个函数。

  • 模型的工具调用消息(msg)和工具返回消息(role: tool)必须成对出现,且 tool_call_id 要匹配,否则 API 报错。

  • 如果同时定义了多个工具,模型会从它们中选择最合适的一个或多个(并行调用)。

2.2 JSON Schema 设计最佳实践

函数参数的 JSON Schema 是模型正确调用的“说明书”。设计好坏直接影响调用准确率。

原则一:描述要“给模型看”,而不是给开发人员看。 每一个 description 都直接影响模型能否正确抽取参数。例如:

// ❌ 差:太泛
"date": {"type": "string", "description": "日期"}

// ✅ 好:明确格式和取值范围
"date": {
    "type": "string",
    "description": "查询日期,格式为 YYYY-MM-DD,必须是今天或未来 7 天内的日期。"
}

原则二:使用枚举(enum)约束有限选项。 如果某个参数只有几个合法值,一定要用 enum,别让模型瞎猜。

"sort_order": {
    "type": "string",
    "enum": ["asc", "desc", "default"],
    "description": "排序方向"
}

原则三:对于复杂对象,用嵌套结构清晰表达。

{
    "type": "object",
    "properties": {
        "filter": {
            "type": "object",
            "description": "过滤条件",
            "properties": {
                "min_price": {"type": "number", "description": "最低价格"},
                "max_price": {"type": "number", "description": "最高价格"},
                "category": {"type": "string", "enum": ["food", "drink", "other"]}
            },
            "required": ["category"]
        }
    }
}

原则四:提供示例(examples)帮助模型理解边界情况。 OpenAI 支持在 Schema 中加 examples 字段(或通过 description 嵌入示例),这能显著提高复杂参数的准确率。


Function Calling 中参数校验应该在哪个阶段做?为什么?

答案:参数校验必须在“应用执行阶段”做,而不是依赖模型生成阶段。永远是“先校验,再执行”。

image.png

为什么不能依赖模型?

  • 模型是概率性的,不可靠:温度不为 0 时,同一输入可能产生不同的参数。即使温度=0,模型也可能出现逻辑幻觉,比如把城市名填成日期。

  • 没有绝对安全的生成:即使用 JSON Schema 约束,模型也可能生成合法的 JSON 但包含不合理的值(比如 "quantity": -100,或者 SQL 注入片段)。

  • 越权风险:模型不知道当前用户的权限边界。一个普通用户可能通过精心构造的 prompt 让模型生成 delete_all_users 函数的调用,只有执行前校验才能拦住。

三层校验体系示例代码:

def execute_tool_safely(func_name, arguments, user_context):
    # 1. 结构校验:用 JSON Schema 验证参数格式
    schema = get_tool_schema(func_name)
    try:
        jsonschema.validate(instance=arguments, schema=schema)
    except jsonschema.ValidationError as e:
        raise InvalidArguments(f"参数格式错误: {e.message}")

    # 2. 业务校验:值范围、业务规则
    if func_name == "transfer_money":
        if arguments["amount"] <= 0:
            raise InvalidArguments("转账金额必须大于 0")
        if arguments["amount"] > user_context.daily_limit:
            raise PermissionDenied("超出每日转账额度")

    if func_name == "query_database":
        if any(keyword in arguments["sql"].upper() for keyword in ["DROP", "DELETE", "UPDATE"]):
            raise InvalidArguments("只允许 SELECT 查询")

    # 3. 权限校验:当前用户能否调用此工具
    if func_name not in user_context.allowed_tools:
        raise PermissionDenied(f"无权调用 {func_name}")

    # 全部通过,真正执行
    return call_actual_function(func_name, arguments)

补充一点:在模型侧也可以做“软校验” 虽然不是安全防线,但在 description 中加上约束(如“金额必须为正数,且不超过用户余额”),能让模型在生成参数时就倾向于合规,减少无效调用次数,降低成本和延迟。这是“用户体验优化”,而“代码硬校验”是“安全保障”,两者并不矛盾,应该兼用。

模型返回的参数和函数签名不匹配,如何处理?

这是 Function Calling 最常见的“翻车现场”:模型返回了 {"city_name": "Beijing"},而你的函数签名是 get_weather(city: str) —— 参数名对不上,代码直接报错。

常见的不匹配类型:

  • 参数名偏差:模型编了一个名字,或者大小写不对(City vs city)。

  • 类型错误:需要 int,模型给了 "3"

  • 缺必填参数:required: ["from", "to"],模型只给了 from

  • 多余参数:模型自己脑补了函数签名里没有的字段。

  • 值不合法:参数名和类型都对,但值在业务上不成立(如负数数量)。

处理策略:防御性编程 + 闭环修正

我们不应该假设模型一次就给出完美参数,而是把“不匹配”当成正常情况,设计一套稳健的处理流水线:

image.png

代码示例:带自愈能力的 Function Calling 执行器

import jsonschema, json

def call_tool_with_recovery(messages, tools, tool_functions, max_retries=2):
    """调用工具,带参数校验和自动修正重试"""
    for attempt in range(max_retries + 1):
        response = client.chat.completions.create(
            model="gpt-4o", messages=messages, tools=tools
        )
        msg = response.choices[0].message
        if not msg.tool_calls:
            return msg.content  # 模型觉得不需要调工具,直接返回文本

        tool_call = msg.tool_calls[0]
        func_name = tool_call.function.name
        try:
            arguments = json.loads(tool_call.function.arguments)
        except json.JSONDecodeError:
            # 连 JSON 都解析失败,告诉模型重来
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": "Error: 参数 JSON 格式无效,请检查后重新生成。"
            })
            continue

        # 1. 拿到该函数的 JSON Schema 进行结构校验
        schema = tools_schema_map[func_name]["parameters"]
        try:
            jsonschema.validate(instance=arguments, schema=schema)
        except jsonschema.ValidationError as e:
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": f"参数校验失败: {e.message}。请修正后重试。"
            })
            continue

        # 2. 业务校验
        try:
            validate_business_rules(func_name, arguments)
        except ValueError as e:
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": f"业务校验失败: {e}。请调整参数后重试。"
            })
            continue

        # 3. 全部通过,执行函数并返回
        result = tool_functions[func_name](**arguments)
        return result

    raise RuntimeError("工具调用失败:超过最大重试次数")

核心思路: 遇到不匹配不要直接崩溃,而是把错误信息作为“tool”角色的消息喂回给模型。模型看到“你刚才的参数校验失败,原因是 xxx”,通常能自我纠正。这就像你告诉同事“这个表没填完”,他会马上补上,而不需要你替他填。

如果重试 2 次仍然失败,进入人工兜底或降级逻辑(如返回一个友好的错误提示给用户)。永远不要信任模型的输出,校验是你掌控安全与稳定性的最后防线。


工具描述写得模糊会导致什么问题,怎么排查?

工具描述(descriptionparameters 中的 description)是模型理解“这个函数是干什么的、参数该怎么填”的唯一依据。描述模糊,模型就会“猜”,而猜往往是错的。

模糊描述引发的三类典型问题:

  1. 选错工具:你写了两个工具 search_productsearch_order,但描述都是“搜索”,模型面对“查一下手机订单”时,有一半概率选错。

  2. 参数胡填:参数 date 的描述是“日期”,模型可能填 "今天""2026/07/02""2026-07-02T00:00:00Z",完全不可控。

  3. 该调不调,不该调瞎调:模型不确定这个工具到底能做什么,干脆不调;或者相反,对明显超出工具能力范围的请求也强行调用,产生幻觉。

排查方法:日志审计 + 回归分析

当你发现 Agent 的工具调用行为异常时,按以下步骤定位:

第一步:开启工具调用日志,锁定失败样本。

记录每一次工具调用的完整上下文:用户原始输入、模型生成的函数名和参数、执行结果、是否成功。

log_entry = {
    "timestamp": now,
    "user_input": user_msg,
    "tool_name": func_name,
    "arguments": arguments,
    "success": False,
    "error": str(e)
}

每天或每周扫描这些日志,找出调用失败率最高的工具或参数。

第二步:将失败样本“喂回”给模型进行归因分析。

用另一个独立的 LLM 调用来诊断问题:

diagnose_prompt = f"""
你是一个工具调用诊断专家。下面是一次失败的工具调用记录:
用户需求:{user_msg}
调用的工具:{tool_name}
传入的参数:{arguments}
工具的描述:{tool_description}
失败原因:{error}

请分析:是工具描述不清晰导致参数填错,还是用户需求本身超范围?
如果是描述问题,请给出具体修改建议。
"""

这能快速区分是“描述模糊”还是“用户输入真的有问题”。

第三步:对比模糊描述和清晰描述的效果。

举个例子,一个模糊的工具定义:

// ❌ 模糊
{
    "name": "get_report",
    "description": "获取报告",
    "parameters": {
        "type": "object",
        "properties": {
            "type": {"type": "string", "description": "报告类型"},
            "date": {"type": "string", "description": "日期"}
        }
    }
}

模型可能把 type 填成 "销售""sales""销售报告" 等各种变体,你的后端代码需要写一堆兼容逻辑。

修改为清晰版本:

// ✅ 清晰
{
    "name": "get_sales_report",
    "description": "获取指定时间范围内的销售数据报表,返回总额、订单数和同比变化率。",
    "parameters": {
        "type": "object",
        "properties": {
            "start_date": {
                "type": "string",
                "description": "报表开始日期,格式 YYYY-MM-DD,如 2026-07-01"
            },
            "end_date": {
                "type": "string",
                "description": "报表结束日期,格式 YYYY-MM-DD,不能早于 start_date"
            }
        },
        "required": ["start_date", "end_date"]
    }
}

改动点:函数名具体化、描述说明输入输出、参数名无歧义、参数描述给出格式和约束。 这类调整能让工具调用的准确率从 70% 迅速提到 95% 以上。

最终结论: 工具描述的清晰度,直接等于模型调用工具的准确度。排查时不要瞎猜,去看日志、做归因、然后用更具体的描述逐步替换模糊的旧版本。这是一个低成本、高回报的优化手段。


Parallel Function Calling 和 Sequential Function Calling 的区别?什么场景下用哪种?

并行调用:模型在一次回复中同时返回多个独立的 tool_calls,你的代码可以同时执行它们,互不依赖。 串行调用:模型先调用工具 A,等拿到 A 的结果后,再根据结果决定调用工具 B,依次进行。

并行 (独立任务):
用户: "北京和上海的天气分别怎么样?"
模型返回:
  ├─ tool_call 1: get_weather("北京")
  └─ tool_call 2: get_weather("上海")
→ 这两个调用可以同时发出,不需要等待彼此。

串行 (依赖任务):
用户: "帮我查一下我最新订单的物流状态。"
模型第一轮: get_user_profile() → 拿到 user_id
模型第二轮: get_latest_order(user_id) → 拿到 order_id
模型第三轮: get_logistics(order_id) → 拿到物流信息
→ 每一步都依赖上一步的结果,必须顺序执行。

工程决策:用哪个?

你可以通过 tool_choiceparallel_tool_calls 参数来控制。但更重要的判断逻辑是:

用并行的情况:

  • 多个工具调用之间完全没有数据依赖。

  • 每个调用的参数都可以在用户输入中直接获取,不需要前一个工具的返回值。

  • 你的目标是降低延迟:同时请求多个服务,等待最慢的那个完成即可。

  • 注意: 即使模型返回了并行调用,你也要在你的代码里用 asyncio.gather 或线程池来真正并发执行,否则就浪费了这个优化。

用串行的情况:

  • 任务有严格的先后顺序,后续调用需要前置调用返回的 ID、token 或其他数据。

  • 你需要做条件判断:比如“先查用户会员等级,如果是 VIP 才查专属折扣,否则查普通价格”。

  • 即使模型以并行方式返回了调用,但如果它们有隐性依赖,你的执行层也应强制串行化或建立依赖图。

一个实用的混合处理代码骨架:

async def execute_tool_calls(messages, tools, tool_map):
    """处理单轮工具调用,支持并行执行"""
    response = await client.chat.completions.create(
        model="gpt-4o", messages=messages, tools=tools
    )
    msg = response.choices[0].message

    if not msg.tool_calls:
        return msg.content

    # 检查是否所有调用都独立(无依赖关系)
    tasks = []
    for tc in msg.tool_calls:
        args = json.loads(tc.function.arguments)
        tasks.append(asyncio.create_task(
            execute_one_tool(tc.function.name, args, tool_map)
        ))

    # 并行执行所有独立工具
    results = await asyncio.gather(*tasks, return_exceptions=True)

    # 把结果追加回消息历史
    messages.append(msg)
    for tc, result in zip(msg.tool_calls, results):
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": str(result) if not isinstance(result, Exception) else f"Error: {result}"
        })
    return messages  # 返回更新后的消息,外层可继续循环

什么场景下必须手动切换为串行?

如果你的工具之间有“隐式依赖”,比如一个工具返回文件 ID,另一个工具需要这个 ID 才能下载,但模型第一次就给出了并行调用,这时候你的执行器应该:

  1. 执行第一个调用。

  2. 把第一个调用返回的 ID 注入到第二个调用的参数中(或者让模型重新生成参数)。

  3. 再执行第二个调用。

换句话说,执行层永远是你说了算,模型只负责“提出调用意图”,最终的执行顺序和并发策略由你的代码编排。 不要在安全性和正确性上让步给模型。

Agent 需要调用一个耗时很长的外部 API(如数据分析),如何设计异步 Function Calling?

核心思路:把一次调用拆成两个工具

Agent 看到的 Function Calling 不再是“一个函数等到天荒地老”,而是两个工具:

  1. launch_analysis(params):提交任务,立即返回 job_id 和状态 pending

  2. get_job_status(job_id):查询任务状态,返回 running/completed/failed,如果有结果就带上。

对 Agent 来说,这就是一个非阻塞的状态机模型,它只需要记住 job_id,在合适的时机去轮询结果。

image.png

这样流程清晰,没有长连接等待,Agent 自己的思考循环也不会卡在某个工具调用上。


几个设计要点(都是实战里淌出来的)

  • 任务状态要持久化 用 Redis + 数据库双写。Redis 做热状态查询,数据库防止重启丢任务。任务元信息(谁提交的、参数、创建时间)一定要落地,方便审计和重试。

  • 轮询策略得控制 不让 Agent 无限高频轮询,我在工具描述里加提示:“结果未就绪时 status 为 running,建议至少间隔 30 秒再次查询”。 同时在 get_job_status 里返回 retry_after 字段(秒数),让 Agent 自己决定等多久,比硬编码聪明。

  • 主动推送 vs 轮询 如果系统允许,我倾向“轮询为主,推送为辅”。 轮询用 get_job_status 就够了,简单万能。 如果外部 API 支持 Webhook,可以在任务完成时回调到我们服务,然后把结果塞进 Agent 的消息上下文(例如用一条 system: "任务 job_042 已完成,结果如下..." 的消息触发一次新的推理),这样用户不用问就能看到结果。不过这对框架有要求,LangChain/LangGraph 那种有状态图的好做,纯手撸就得小心上下文管理。

  • 超时与幂等 任务可能等很久,get_job_status 要设置一个“终态过期时间”,比如 24 小时后状态自动置 expired,避免 Agent 追问一个已死的任务。 launch_analysis 要做幂等键,防止用户点好几次提交重复任务。

  • 工具定义举例(OpenAI 格式)

{
  "name": "launch_analysis",
  "description": "提交一个异步数据分析任务,立刻返回任务ID。",
  "parameters": {
    "type": "object",
    "properties": {
      "dataset": {"type": "string"},
      "method": {"type": "string"}
    },
    "required": ["dataset"]
  }
}
{
  "name": "get_job_status",
  "description": "查询异步任务的执行状态。如果完成,result字段会包含分析结果。",
  "parameters": {
    "type": "object",
    "properties": {
      "job_id": {"type": "string"}
    },
    "required": ["job_id"]
  }
}

这种设计下 Agent 的“智能”在哪

Agent 不只是呆板轮询,它可以做决策:

  • 如果 status: "running"eta 很长,Agent 可能主动说“大概还要 8 分钟,要不我先帮你做点别的?”

  • 如果 status: "failed",Agent 可以根据错误码建议重试或调整参数。

  • 它甚至可以在一次对话里管理多个并行 job_id,像个小调度器。


这样下来,耗时 API 就不再是 Agent 的阻塞点了。你那边业务场景里,数据分析任务时长波动大吗?如果经常有过长的任务(>30 分钟),我一般会再加一个 cancel_job 工具,让用户能主动取消。想听这块的话我可以再展开聊聊。


工具的原子性与单一职责设计原则

工具粒度过粗或过细分别有什么问题?

粒度过粗:一个工具包含太多逻辑,复用性差,出错难定位,模型对工具语义理解模糊。粒度过细:Agent 需要多轮调用才能完成任务,延迟高、Token 消耗大,中间步骤增多导致出错概率上升。


在 AI Agent 的工具设计中,如何把握工具粒度?原子工具和复合工具各适合什么场景?

1️⃣ Common Answer

工具要遵循单一职责原则,每个工具只做一件事。粒度太粗复用性差,太细调用次数太多效率低。原子工具适合灵活场景,复合工具适合固定流程。命名要清晰,像 search_webget_weather 这种风格,让 LLM 一看就懂。

2️⃣ Impressive Answer

我会从 3 个角度来分析这个问题:

  1. 粒度过粗的工程代价。比如一个 analyze_and_report 工具内部包含数据拉取、分析、格式化、发邮件四步。问题在于:其他任务只需要数据拉取时这个工具用不了;出错后无法定位是哪一步;模型对工具语义理解变模糊,调用准确率下降。

  2. 粒度过细的工程代价。比如 open_fileread_lineclose_file 各自独立。Agent 需要多轮调用完成一件事,延迟和 Token 消耗都上去了;上下文被大量中间工具结果填满,影响后续推理质量。

  3. 实际工程中的分层策略。我比较认可的做法是分三层:原子工具层(execute_sqlhttp_get),保证可组合性;复合工具层(query_user_orders,内部封装 SQL 构建+执行+格式化),减少调用步骤;动态工具注入,根据当前任务上下文只暴露相关工具,避免工具列表过长导致模型选择混乱。实践经验是:先用原子工具跑通流程,观察 Agent 的高频调用组合,再把那些总是连续调用的工具封装成复合工具。命名方面,动词+名词结构最清晰,避免缩写,相似功能的工具要有明显区分词。

3️⃣ Key Differences

查看内嵌表格


业务新增了一个"查询并汇报用户订单"需求,你怎么决定设计原子工具还是复合工具?

1️⃣ Common Answer

如果只有这一个需求,可以直接做一个复合工具,把查询和汇报都放进去,这样 Agent 调用一次就完事了,比较简单。

2️⃣ Impressive Answer

  1. 先问复用性。如果"查询订单"和"汇报结果"分别还有其他独立使用场景(比如其他任务只需要查询不需要汇报),就应该拆成原子工具 query_orders + format_report,保留复用能力。

  2. 再评估频次。如果这个"查询+汇报"的组合在 Agent 运行中出现频率极高,可以在原子工具基础上封装一个复合工具 query_and_report_orders,两层都保留,让 Agent 根据任务自行选择。

  3. 工程落地。先上原子工具版本,上线后通过 LangSmith 观察 Agent 的实际调用模式,如果发现两个工具总是连续调用,再封装复合工具,这样决策有数据支撑,不是拍脑袋。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


7.2 工具可靠性与安全

工具调用的错误处理与智能重试策略


基础题:工具调用失败后应该怎么处理?

难度级别:⭐(错误处理基础、重试机制、降级策略)

工具调用失败后,把错误信息作为 tool role 的 content 反馈给 LLM,让它根据错误自主调整参数或换工具。同时需要设置最大重试次数(通常 2-3 次),超过次数后做降级处理,返回描述性的降级结果而不是直接抛出空值。


进阶题:当 Agent 调用工具返回错误时,应该如何设计错误处理和重试机制?LLM 如何利用错误信息来纠正自身行为?

难度级别:⭐⭐(错误分类处理、错误信息面向 LLM 设计、重试策略、防止循环)

1️⃣ Common Answer

工具调用出错时,把错误信息返回给 LLM 让它重新生成参数或换工具。设置最大重试次数,比如 3 次,超过就降级处理。错误信息要包含具体原因,这样 LLM 才能理解。重试时加指数退避,避免频繁请求被限流。

2️⃣ Impressive Answer

我会从 3 个角度来思考这个问题:

  1. 错误分类,策略不同。参数错误(格式不对、校验失败):直接把错误作为 Observation 反馈给 LLM,让它自主修正,不需要重试间隔。业务逻辑错误(无结果、权限不足):反馈给 LLM 后让它决定是换参数、换工具还是告知用户。基础设施错误(网络超时、服务不可用):LLM 处理不了,在工具执行层做指数退避重试,对 LLM 透明。

  2. 错误信息要面向 LLM 设计。不要给 DB_ERROR_1062: Duplicate entry 这种工程师调试信息,而是"该用户 ID 已存在,无法重复创建。请确认是否需要更新已有记录,或检查传入的 user_id 是否正确。"LLM 才能从中推理出正确的修正方向。

  3. 防止无限重试循环。LLM 可能被错误信息引导进入循环(反复换格式但问题不在格式上)。需要在 Agent 循环层面做全局步骤计数限制,并在 Prompt 中明确说明"如果同一工具连续失败 2 次,请换策略或直接告知用户"。写操作最多 1 次重试(防止重复写入),读操作可以 3 次;超过次数后返回描述性降级结果,让 Agent 能继续推进任务。

3️⃣ Key Differences

查看内嵌表格


场景题:Agent 在调用 search_web 工具时,连续返回超时错误,如何处理?

难度级别:⭐⭐(基础设施错误处理、降级策略、避免影响 Agent 主流程)

1️⃣ Common Answer

超时了就重试几次,还是不行就告诉用户搜索失败了,让用户自己手动搜索。

2️⃣ Impressive Answer

  1. 在工具执行层做指数退避重试,对 LLM 透明。超时属于基础设施错误,LLM 无法从中学到有用信息,应该在工具内部做 1s → 2s → 4s 的退避重试,最多 3 次,LLM 感知不到这个过程。

  2. 超过重试次数后,返回描述性降级结果。不要返回空值或直接抛出异常,而是返回 {"error": "搜索服务暂不可用,以下是基于已有知识的回答建议"},LLM 看到这个能继续推进任务,而不是卡死。

  3. 同时触发 P0 告警。连续超时说明外部依赖有问题,需要立即通知 on-call,告警内容带上过去 5 分钟的失败次数和失败率,避免告警风暴。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


代码执行工具的安全沙箱设计


基础题:为什么代码执行工具需要沙箱隔离?

难度级别:⭐(安全隔离基础、常见风险)

LLM 生成的代码可能包含恶意操作(删除文件、读取敏感信息、消耗系统资源),直接在宿主机执行风险极高。沙箱通过隔离执行环境,让代码无法影响宿主机,同时限制资源使用,防止 DoS 攻击。


进阶题:在 AI Agent 中集成代码执行能力时,如何设计安全沙箱?Docker 容器隔离和 E2B 沙箱各有什么特点?

难度级别:⭐⭐⭐(Docker vs E2B 对比、完整资源限制、多层防护设计、黑名单局限性)

1️⃣ Common Answer

用 Docker 容器隔离代码执行环境,每次执行创建新容器,执行完销毁,这样代码出问题也不影响宿主机。用 --memory--cpus 限制资源,设置超时时间。再做个黑名单过滤 rm -rfos.system 这类危险操作。E2B 是专门的沙箱服务,比自己搭 Docker 更方便。

2️⃣ Impressive Answer

我会从 3 个角度来思考这个问题:

  1. Docker vs E2B 的选型对比。Docker 自托管:完全可控、成本低,但容器启动延迟 500ms-2s,需要自己维护镜像安全更新和容器逃逸补丁,适合数据安全要求严格、不能发给第三方的场景(金融、医疗)。E2B:基于 Firecracker 微虚拟机,启动延迟低于 200ms,天然多租户隔离,提供文件系统持久化和流式输出等 Agent 场景所需能力,适合快速落地、对延迟敏感的 Agent 产品。

  2. 资源限制要做完整清单,不能只限内存和 CPU。还需要限制进程数(pids_limit 防 fork bomb)、完全断网或白名单网络(network_mode: none)、根文件系统只读(read_only: True)、删除所有 Linux capabilities(cap_drop: ALL)。只靠内存和 CPU 限制,很容易被绕过。

  3. 防护要分多层,黑名单是最弱的防线import('os').system('rm -rf /') 这类变形写法黑名单根本穷举不完。正确做法是:执行前用 AST 解析检测危险模块导入(比字符串匹配更难绕过);执行中用容器隔离+syscall 白名单(seccomp profile);执行后对输出做内容安全扫描防止信息泄露。黑名单只做第一道粗筛。

3️⃣ Key Differences

查看内嵌表格


场景题:用户让 Agent 执行一段 Python 脚本,脚本里有 import socket 和网络请求,怎么处理?

难度级别:⭐⭐⭐(AST 静态分析、网络隔离策略、风险评估与放行机制)

1️⃣ Common Answer

import socket 有风险,可以在黑名单里加上 socket,发现就拒绝执行,告诉用户不允许网络操作。

2️⃣ Impressive Answer

  1. 静态分析层先检测。用 AST 解析脚本,识别到 import socket 后,不是直接拒绝,而是判断业务场景——如果这个 Agent 本来就需要联网能力(比如爬虫 Agent),可以放行到受控网络(白名单出口);如果是纯计算类 Agent,则拒绝并告知用户"网络访问不在允许范围内"。

  2. 执行层用网络隔离兜底。无论静态分析是否通过,容器都配置为自定义网络(白名单出口 IP 或域名),即使代码里有恶意网络请求,容器也无法连接到非白名单地址,保证纵深防御。

  3. 给用户友好的反馈。不要直接返回"安全校验失败",而是说"检测到网络访问请求,当前沙箱环境限制为只读计算,如需联网操作请联系管理员开启对应权限",提升用户体验。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


Text-to-SQL 工具的实现与幻觉防护


基础题:Text-to-SQL 工具的基本实现思路是什么?

难度级别:⭐(Schema 注入、SQL 安全约束、基本流程)

把数据库表结构(Schema)注入 Prompt,让 LLM 根据用户自然语言生成 SQL,使用只读账号执行,把结果返回给用户。安全上要限制只生成 SELECT 语句,禁止 DDL 和 DML 操作。


进阶题:如何实现一个生产可用的 Text-to-SQL 工具?Schema 注入、SQL 安全校验和自动纠错循环怎么做?

难度级别:⭐⭐⭐(Schema 格式设计、表召回、多层安全校验、自纠错循环设计)

1️⃣ Common Answer

Text-to-SQL 就是让 LLM 把问题转成 SQL。需要把表结构告诉 LLM,让它知道有哪些表和字段,然后生成 SQL。安全方面只让它生成 SELECT,用只读账号执行。SQL 执行失败了,把错误信息返回给 LLM 让它重新生成,通常重试一两次就行。

2️⃣ Impressive Answer

我会从 3 个角度来拆解生产可用的 Text-to-SQL:

  1. Schema 注入格式是核心。仅列字段名效果很差,生产中需要带类型、注释、示例值、外键关系。比如 status: ENUM('pending','paid','cancelled'), 订单状态 配合 Sample data: status 常见值为 'paid',能显著提升 SQL 生成准确率。另外,大型系统有几百张表,不能全部注入,要先做表召回——用向量检索或关键词匹配找出最相关的 3-5 张表,只注入这些表的 Schema,避免 context 爆炸且让模型注意力更集中。

  2. SQL 安全校验要做三层。语法层:用 sqlparse 解析 SQL,确认是 SELECT,拒绝 DDL/DML;语义层:检查危险函数(LOAD_FILEINTO OUTFILE)和全表扫描风险(超大表无 WHERE 子句);执行层:只读账号+强制设置 statement_timeout,防止慢查询打垮数据库。只靠只读账号远远不够。

  3. 自纠错循环有个关键细节。SQL 执行失败后,要把原始 SQL 和错误信息一起传给 LLM,同时加指令"请分析错误原因,修正 SQL,不要改变查询意图"。只给错误信息不给原始 SQL,LLM 容易生成一个完全不同的 SQL,偏离用户意图。最多重试 2 次,超过后返回友好提示。另外查询结果为空时,要区分"确实无数据"和"SQL 写错了导致无结果",必要时让 LLM 验证 SQL 的语义是否符合问题逻辑。

3️⃣ Key Differences

查看内嵌表格


场景题:用户问"查一下最近一个月销售额最高的 Top10 产品",LLM 生成的 SQL 返回空结果,怎么处理?

难度级别:⭐⭐⭐(空结果区分、语义验证、用户确认机制)

1️⃣ Common Answer

SQL 返回空就告诉用户没有数据,或者让 LLM 重新生成一个 SQL 试试。

2️⃣ Impressive Answer

  1. 先区分"真没数据"还是"SQL 写错了"。对结果为空的 SQL,让 LLM 做语义验证:把生成的 SQL 和原始问题一起传给模型,问"这条 SQL 是否正确表达了用户的查询意图?"如果模型判断 SQL 逻辑有问题,触发一次纠错重试。

  2. 检查常见 SQL 陷阱。比如日期范围写反、时区问题、JOIN 条件不对导致笛卡尔积被 WHERE 过滤掉等。可以在纠错提示中加上"请检查日期范围、JOIN 条件是否正确"作为提示。

  3. 给用户透明的反馈。如果多次重试仍为空,要告诉用户"根据您的问题生成的查询未返回结果,可能是近一个月确实无销售记录,也可能是查询条件需要调整,生成的 SQL 如下,您可以确认",把 SQL 暴露给用户做最终判断,避免黑盒。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


工具调用的并发安全性设计


基础题:什么是工具的幂等性?为什么在 Agent 中很重要?

难度级别:⭐(幂等性概念、重试场景下的必要性)

幂等性是指同一操作执行多次,结果和执行一次相同。在 Agent 中,工具可能因错误被重试,如果写操作不幂等,就会造成重复写入(如重复创建订单)。通过业务幂等键或唯一约束保证幂等性是写操作工具的基本要求。


进阶题:当 Agent 并行调用多个工具时,如何保证并发安全性?工具幂等性设计和事务管理应该怎么做?

难度级别:⭐⭐⭐(并发竞争场景、幂等键设计、单工具单事务原则、信号量限流)

1️⃣ Common Answer

并行调用工具时可能产生竞争条件,用锁保护共享资源就行。幂等性就是同一操作执行多次结果一样,查询天然幂等,写操作用唯一约束防重复。数据库操作要用事务保证原子性,出错了回滚。

2️⃣ Impressive Answer

我会从 3 个角度来分析:

  1. 常见竞争场景要心中有数。读-改-写竞争(两个工具并发读同一计数器各自加 1 写回,结果少加一次);文件系统竞争(两个工具同时写同名临时文件互相覆盖);外部 API 限流(多工具并发调同一第三方 API 触发频率限制)。不同场景对应不同的解法,不能一概而论。

  2. 幂等性设计要分层。天然幂等:GET 类查询无需处理。业务 ID 幂等:写操作携带 idempotency_key,服务端先查是否已存在,存在则返回已有结果,不存在才创建。条件写幂等:用乐观锁(UPDATE ... WHERE version = ?)或数据库唯一约束防止并发重复写入。事务管理上,遵循单工具单事务原则,不跨工具共享事务上下文,避免并发时事务边界混乱;只读工具显式用只读事务,数据库可做更好的并发优化。

  3. 框架层面用信号量控制并发数。用 asyncio.Semaphore 限制同时执行的工具数量(比如最多 5 个并发),防止资源过载。asyncio.gatherreturn_exceptions=True,保证某个工具失败不中断其他工具的执行,最后统一处理失败项。

3️⃣ Key Differences

查看内嵌表格


场景题:Agent 同时调用"创建任务"和"更新任务状态"两个工具,如何避免数据不一致?

难度级别:⭐⭐⭐(并发顺序依赖、任务依赖建模、事务边界设计)

1️⃣ Common Answer

这两个操作有顺序依赖,必须先创建再更新。可以用锁或者串行执行,等创建完了再调更新接口。

2️⃣ Impressive Answer

  1. 识别依赖关系,不并发执行有依赖的工具。"更新任务状态"依赖任务已存在,这两个工具本身不应该并发调用。在 Agent 的任务规划层,通过 DAG 建模工具依赖关系,有依赖的工具串行执行,无依赖的工具才并发执行。

  2. "创建任务"工具保证幂等。携带 idempotency_key,即使因网络问题被重试,也不会创建出两个相同任务,确保"更新任务状态"时操作的是同一个任务 ID。

  3. 在数据库层做约束兜底。"更新任务状态"的 SQL 加上 WHERE task_id = ? AND status != 'completed' 这类条件,防止并发下状态被错误覆盖,数据库层做最后一道防护。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


7.3 工具工程实践

工具结果摘要:处理大型工具返回值


基础题:工具返回内容过长超出 Token 限制时,最简单的处理方式是什么?

难度级别:⭐(截断基础、Token 限制意识)

最直接的方式是按条数或字符数截断,比如搜索结果只保留前 5 条,并在末尾注明"共 N 条记录"。但要避免按字符截断结构化数据,否则会破坏 JSON 结构导致 LLM 解析失败。


进阶题:当工具返回大量数据时,如何避免超出 Token 限制?截断、摘要策略和 Token 预算应该怎么设计?

难度级别:⭐⭐(按数据类型截断、摘要成本与风险、动态 Token 预算分配)

1️⃣ Common Answer

工具返回内容太长就直接截断到一定字符数,或者只取前 N 条。也可以用 LLM 对返回内容做摘要,把长内容压缩后注入上下文。Token 预算方面,给工具结果保留 30-40% 的 context 空间,不让对话历史把 context 全占满。

2️⃣ Impressive Answer

我会从 3 个角度来思考这个问题:

  1. 截断策略要按数据类型来。搜索结果:按条数截断,每条保留标题+摘要+URL,丢弃正文,不要按字符硬截。结构化数据(JSON/表格):先过滤掉和当前问题无关的字段,再考虑行数截断,末尾注明"共 N 条,以上为前 X 条"。文件内容:不要直接截头,代码文件按函数/类分割,文档按段落分割,只取与问题最相关的片段(本质是 RAG 的思路)。

  2. LLM 摘要有成本和风险,要谨慎使用。摘要本身要额外调用一次 LLM,高频场景成本不低;精确数字和代码摘要后容易失真,这类内容不适合摘要,应该直接截断保留原文。需要摘要时,用小模型(如 GPT-4o-mini)而不是主模型,降低延迟和成本。

  3. Token 预算要动态计算,而不是固定比例。动态分配逻辑:模型上下文限制 - 系统 Prompt tokens - 对话历史 tokens - 预期输出 tokens = 可用空间,给工具结果最多 60% 的可用空间,并保证至少 500 token。多工具并发时,按工具结果对当前问题的重要性优先分配预算。另外,用 tiktoken 精确计算 token 数,不要用字符数估算,中英文混合场景下两者差异很大。

3️⃣ Key Differences

查看内嵌表格


场景题:Agent 调用了一个返回 200 条 JSON 记录的数据库查询工具,如何处理这个结果?

难度级别:⭐⭐(结构化数据处理、字段过滤、Token 预算估算)

1️⃣ Common Answer

200 条太多了,截取前 20 条传给 LLM 就行,或者让 LLM 对这 200 条做摘要。

2️⃣ Impressive Answer

  1. 先做字段过滤,再做行数截断。不要直接截前 N 条,先分析当前问题需要哪些字段,比如用户问"最近订单的状态分布",只需要 statuscreated_at 字段,把其他字段过滤掉可以把数据量压缩 80% 以上。字段过滤后再按行数截断,末尾注明"共 200 条,以上为前 50 条,仅展示 status 和 created_at 字段"。

  2. 用 tiktoken 估算截断后的 token 数。不要用字符数估算,JSON 结构有大量括号和键名,token 消耗远高于纯文本。估算后和当前可用 token 预算对比,动态决定保留多少行。

  3. 考虑让 SQL 层做聚合。如果问题是统计类的(分布、Top N、总数),应该在工具层让数据库直接做 GROUP BYLIMIT,而不是把原始数据全拉出来再在 Python 层处理。这是从根本上减少数据量的最优解。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


工具调用的可观测性:追踪每次调用的输入输出


基础题:工具调用的日志应该记录哪些字段?

难度级别:⭐(结构化日志基础、必要字段)

至少要记录:工具名称、输入参数(敏感信息需脱敏)、返回值大小(而非全量内容)、执行耗时(毫秒)、会话 ID(session_id)、Agent 运行 ID(run_id,用于关联同一次运行中的所有工具调用),以及成功/失败状态和错误信息。


2、进阶题:在生产环境中,如何对 Agent 工具调用进行全面的可观测性建设?结构化日志、告警和 LangSmith 如何落地?

难度级别:⭐⭐(run_id 链路关联、分级告警避免告警风暴、LangSmith 核心功能、自建 vs LangSmith 选型)

1️⃣ Common Answer

可观测性就是能看到 Agent 运行时发生了什么。工具调用的日志要记录工具名、输入参数、返回值和执行耗时,方便排查问题。出现异常时触发告警,比如工具连续失败或耗时超阈值就发通知。LangSmith 是 LangChain 官方追踪工具,能看到每次工具调用的详细信息。

2️⃣ Impressive Answer

我会从 3 个角度来拆解生产级可观测性:

  1. 结构化日志的关键设计点run_id 是核心字段,用于关联同一次 Agent 运行中的所有工具调用,没有它就无法从日志中还原完整调用链路。返回值记录大小而不是全内容,避免日志量爆炸;需要 debug 时,把完整返回值存对象存储,日志里记录引用。敏感参数(API 密钥、用户身份信息)必须脱敏后再写日志。

  2. 告警策略要分级,避免告警风暴。P0 即时告警:工具成功率 5 分钟内低于 80%,有系统级故障。P1 延迟告警:某工具 P99 耗时超阈值(外部 API 超过 10s),影响用户体验。P2 趋势告警:错误率 1 小时内持续上升,可能是数据问题或外部降级。告警要带聚合信息("search_web 过去 5 分钟失败 23 次,失败率 45%,涉及 12 个 session"),不要每次失败都发一条。

  3. LangSmith 的核心价值和选型。最有价值的三个功能:Trace 视图(树状调用链路,排查幻觉或工具选错效率极高);Dataset 积累(一键把有问题的 Trace 加入数据集,用于 Prompt 优化和回归测试);工具调用统计(按工具名过滤,快速看哪个工具最容易失败)。有数据合规要求不能发给第三方的,用 OpenTelemetry + Jaeger 做链路追踪,配合 ELK 做日志聚合,能覆盖同等能力。

3️⃣ Key Differences

查看内嵌表格


场景题:生产中某个工具最近错误率上升,但没有触发告警,如何排查?

难度级别:⭐⭐(告警盲区分析、日志查询、链路追踪排查)

1️⃣ Common Answer

看一下日志,找到最近的错误记录,看看错误信息是什么,然后修复对应的问题。告警没触发可能是阈值设置太高了。

2️⃣ Impressive Answer

  1. 先分析告警盲区的原因。可能是告警阈值设置过高(比如失败率要超过 50% 才告警),或者是 P2 趋势告警没有配置(只有瞬时阈值,没有持续上升检测)。先检查告警规则是否合理,这次排查完后要补充 P2 趋势告警。

  2. 用结构化日志快速定位。在日志系统按 tool_name = 'xxx' + 时间范围过滤,聚合看错误率趋势图,确认是什么时间点开始上升。再按 error_type 分组,看是某一类错误集中出现,还是随机分布。如果是集中出现,大概率是某个依赖变更或数据问题。

  3. 用 run_id 还原完整上下文。找到几个失败的 tool_call_id,通过 run_id 关联拉出完整的 Agent 运行链路,看失败前的工具调用序列,判断是工具本身的问题还是上游传入的参数异常导致的。

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格