Function Calling 幻觉与超时降级排查

约 25 分钟

排查和解决工具调用入参不符合 Schema、工具响应超时及死循环调用的典型故障。

上一课,我们基于 Cordis 事件系统梳理了 DeepSeek Harness 的生命周期钩子机制,掌握了 emit、waterfall、serial、parallel 与 bail 五种事件分发模式的核心契约,并在此基础上实现了一个融合白名单校验拦截与敏感信息脱敏改写的安全审计插件,为大模型输入输出与工具调用打通了可逆的审计管道。正是这条管道,让我们得以清晰观察到工具调用(Function Calling)链路中真实发生的问题。

在真实生产环境中,大模型输出的非确定性、外部网络抖动以及循环决策逻辑常常引发各类异常。典型的故障主要集中在三个维度:

  1. 入参幻觉与 Schema 校验失败:模型生成的 JSON 参数不满足 defineTool 中定义的 parameters 规则(例如缺少必填字段、类型错误或字段命名偏差)。
  2. 工具响应超时与无响应挂起:外部依赖服务延迟陡增或发生死锁,工具执行未设超时上限,导致整个 Harness 任务流停滞。
  3. 多轮调用死循环与 Token 耗尽:智能体在 ReAct 循环中无法根据工具返回获得有效收敛信号,不断发起重复或无效的工具调用,迅速消耗上下文配额。

本节将沿用上一课建立的审计视角,针对这三类高频故障,深入剖析其底层原因,提供可执行的排查路径、熔断策略与代码修复方案。

遇到问题:模型在尝试触发工具时,控制台抛出 Schema 校验异常(如 ValidationError: Invalid tool call arguments 或 Missing required parameter),导致工具未能进入 execute 函数,智能体收到校验失败的错误反馈或直接中断。

可能原因

  • 工具定义中的 parameters 字段描述模糊或缺少明确的类型约束,导致模型推断出的 JSON 结构存在幻觉。
  • 复杂嵌套对象未严格声明字段层级,模型将嵌套字段拍平成一级入参传递。
  • 字段命名存在歧义或与提示词中的业务概念不一致,模型输出了同义词别名(如将 user_id 写成 userId 或 uid)。

排查步骤

  1. 使用 defineTool 显式声明 parameters 的 type、required 以及高辨识度的 description 说明。
  2. 检查 parameters 中的必填字段声明,确保 required 属性为布尔值或显式数组,杜绝不合法的 Schema 结构。
  3. 在 output.render 和错误处理管道中将校验错误以结构化文本形式回传给模型,提示模型根据错误信息自愈修正入参。

排查并加固 defineTool 的入参 Schema 与自愈反馈机制

  1. 1

    检查并规范工具插件中的 defineTool 参数定义,确保参数类型与字段描述精确无歧义。

    typescript
    import type { Context } from '@deepseek-ai/cordis'
    import { defineTool } from '@deepseek-ai/dsh-tools'
    
    export const name = 'robust-tool-demo'
    export const inject = ['tools']
    
    export function apply(ctx: Context) {
      ctx.tools.register(defineTool({
        name: 'query_user_profile',
        description: '根据用户唯一 ID 查询用户画像详细信息。请勿传入用户名。',
        parameters: {
          user_id: {
            type: 'string',
            required: true,
            description: '用户的唯一识别号(UUID 格式,例如 usr-10293)'
          },
          include_history: {
            type: 'boolean',
            required: false,
            description: '是否包含历史交互记录,默认 false'
          }
        },
        output: {
          schema: { type: 'object' },
          render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }]
        },
        async execute(args) {
          if (!args.user_id || typeof args.user_id !== 'string') {
            throw new Error(`参数 user_id 校验失败: 必须为非空字符串,实际收到 ${JSON.stringify(args.user_id)}`)
          }
          return { status: 'ok', data: { id: args.user_id, active: true } }
        }
      }))
    }
  2. 2

    在插件的 execute 方法中捕获业务校验错误,将错误转为模型可识别的提示文本,而非直接崩溃抛出未知未捕获异常。

    typescript
    async execute(args) {
      try {
        return await fetchUserProfile(args.user_id, args.include_history)
      } catch (err: any) {
        // 将结构化错误信息返回给模型上下文,促使模型纠正参数后重新调用
        return {
          error: true,
          message: `工具执行失败: ${err.message}。请核对 user_id 格式后重试。`
        }
      }
    }

你应该看到

当大模型传入错误或缺失参数时,框架能够精准拦截并输出可读的提示信息,引导模型在下一轮调用中自行修正参数。

遇到问题:工具执行耗时过长或下游 API 无响应,整个 Harness 对话卡在 tool_call 阶段无任何日志输出,最终触发网络超时或进程挂死。

可能原因

  • 工具实现中发起了无超时控制的异步 I/O 请求(如未配置 timeout 的 HTTP 请求或数据库死锁)。
  • Harness 配置中未加载或未启用工具超时防护插件,缺失全局截止时间(Deadline)机制。

排查步骤

  1. 在插件中利用 Promise.race 或 AbortController 为所有异步操作设置严格的超时中断机制。
  2. 配置工具调用超时策略插件,确保超时时产出标准错误信息,使得模型能够执行降级分支而非永久挂死。

配置工具执行超时熔断与自动降级策略

  1. 1

    使用 Promise.race 封装带超时限制的工具执行器,在设定的毫秒阈值内未返回时主动中止并返回降级错误。

    typescript
    function withTimeout<T>(promise: Promise<T>, timeoutMs: number, toolName: string): Promise<T> {
      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          reject(new Error(`Error: tool call timed out after ${timeoutMs}ms [tool: ${toolName}]`))
        }, timeoutMs)
    
        promise
          .then((res) => { clearTimeout(timer); resolve(res) })
          .catch((err) => { clearTimeout(timer); reject(err) })
      })
    }
  2. 2

    在 defineTool 的 execute 实现中接入 withTimeout,并提供降级返回以保证对话流程继续向下推进。

    typescript
    async execute(args) {
      const TIMEOUT_MS = 5000
      try {
        return await withTimeout(doRemoteFetch(args.endpoint), TIMEOUT_MS, 'remote_fetch')
      } catch (err: any) {
        return {
          degraded: true,
          fallback_message: `远程接口响应超时 (${TIMEOUT_MS}ms),请向用户说明当前数据暂不可用并提供离线建议。`,
          error: err.message
        }
      }
    }

你应该看到

当远端请求超过指定毫秒数时,工具立即触发超时熔断,返回预设的降级消息,智能体基于降级结果向用户输出友好提示。

遇到问题:智能体陷入死循环调用同一个或一组工具,单次问答连续执行数十步(Step),Trajectory 轨迹中工具调用入参几乎重复,Token 消耗急剧攀升。

可能原因

  • 工具返回值未给模型提供足够的终止判据,模型认为信息不完整因而反复重试。
  • ReactLoopAgent 循环缺乏单轮最大步数(maxSteps)熔断约束,且工具未声明 concludesTurn 标识。
  • 每轮迭代将全部历史消息重新累加构造请求,导致上下文膨胀加速 Token 消耗。

排查步骤

  1. 在工具返回结果中显式补充收敛状态(如 is_final、completed: true)以引导模型生成最终回复。
  2. 利用事件钩子或配置项对单轮执行步数进行熔断拦截,超过最大步数强制终止循环。
  3. 对重复调用同一工具且参数相同的行为进行去重校验,检测到连续重复调用时强制拦截。

利用事件钩子实现多轮调用步骤熔断与死循环拦截

  1. 1

    编写一个监控智能体单轮执行步数的事件拦截插件,记录当前轮次下的工具调用次数并设置最大阈值。

    typescript
    import type { Context } from '@deepseek-ai/cordis'
    
    export const name = 'turn-loop-guard'
    export const inject = ['tools']
    
    export function apply(ctx: Context) {
      let currentStepCount = 0
      const MAX_STEPS = 5
    
      // 监听轮次启动事件重置计数器
      ctx.on('agent/turn-start' as any, () => {
        currentStepCount = 0
      })
    
      // 在工具执行前进行步数熔断检查
      ctx.on('tool/before-execute' as any, (session: any) => {
        currentStepCount++
        if (currentStepCount > MAX_STEPS) {
          throw new Error(`[熔断保护] 单轮工具调用步数已达上限 (${MAX_STEPS}),强制终止以防止死循环。`)
        }
      })
    }
  2. 2

    在工具完成核心任务后,在返回值中明确说明任务已完成,并在 output 渲染中加入终止提示。

    typescript
    output: {
      schema: { type: 'object' },
      render: (_args, value) => [
        { 
          type: 'text', 
          text: `执行结果: ${JSON.stringify(value.data)}。提示:该查询已获得全部所需信息,请直接回答用户。` 
        }
      ]
    }

你应该看到

当智能体单轮调用超过预设的最大步数阈值时,熔断机制立即生效并阻断后续工具调用,防止 Token 产生无意义的指数级消耗。

自检:在 DeepSeek Harness 中,当发现智能体因为某工具调用陷入多轮重复执行且无法收敛时,应从哪些层面进行定位与修复?
  1. 检查工具返回内容:确保工具返回值明确包含任务完成信号(如 concluded 或清晰的无歧义结果),避免因返回空数据或模糊信息导致模型反复重试;
  2. 配置单轮步骤上限:通过事件钩子或 Agent 设置最大调用步数限制(maxSteps),超出时主动熔断;
  3. 审查 Schema 与提示词:通过 Trajectory 追踪模型在每一轮的 reasoning 思维链,修正工具 description 和参数约束,消除模型认知偏差。