☰
[DeepSeek Harness深度拆解-11]深入探讨工具定义、约束和执行防御
2026/10/21 5:33:30 网站建设 项目流程

DeepSeek Harness深度拆解-10:工具如何定义和注册主要以实例的方式演示了如何定义和注册一个表示工具定义的ToolDefinition,如何利用注册的ToolRestriction来控制工具针对LLM的可见性,以及如何利用注册的ToolGuard作为执行工具前的一道防线,决定当前的工具是否真应该执行。在本篇文章我们将深入ToolDefinition、ToolRestriction和ToolGuard分别是个怎样的对象,以及defineTool函数是如何创建ToolDefinition的。

1. 工具的定义

调用ToolRuntime的register方法注册的工具体现为ToolDefinition接口表示的工具定义。该接口是对ToolSchema接口的扩展,后者是对工具Schema的描述,也是最终LLM需要的那部分内容。

exportinterfaceToolSchema{name:stringdescription:stringparameters:Record<string,unknown>}

ToolSchema接口提供的三个字段,分别表示工具的名称、描述和输入参数的Schema。ToolDefinition在这三者基础上提供了如下字段和方法。

exportinterfaceToolDefinitionextendsToolSchema{readonlyoutput:ToolOutputDefinitionexecute(args:unknown,exec:ToolRunContext):Promise<unknown>finalizeContent?(exec:Readonly<ToolExecution>,result:Readonly<ToolExecutionResult>):ContentBlock[]|undefinedtimeoutMs?:numberisConcurrencySafe?(args:unknown):booleanpresentCall?(args:unknown):ToolCallView|undefinedpresentResult?(args:unknown,result:ToolResult):ToolResultView|undefined}

各字段和方法说明如下:

  • output: 针对工具输出的定义;
  • execute:用来执行工具的异步方法,两个参数分别表示执行工具提供的参数和工具执行上下文;
  • finalizeContent:对工具执行的结果(体现为一个ToolExecutionResult对象)作最后处理,用来修正最终作为工具结果消息内容的ContentBlock列表;
  • timeoutMs:设置的工具执行超时时限(单位毫秒);
  • isConcurrencySafe:判断这一次工具调用能否与同一批次里的其他工具调用并行执行;
  • presentCall&presentResult: 为了将工具执行和执行结果在UI生呈现出来,这两个方法分别以ToolCallView和ToolResultView形式返回对应的视图。

1.1 ToolOutputDefinition

output字段返回的针对工具输出的定义对应如下这个ToolOutputDefinition接口,该接口提供的成员解决了:在工具执行并返回了一个JSON之后,这个值如何被校验,如何变成模型内容,以及如何被展示。

exportinterfaceToolOutputDefinition{readonlyschema:JsonSchemaNoderender(args:unknown,value:JsonValue):ContentBlock[]presentationMeta?(args:unknown,value:JsonValue):JsonValue}

三个成员对应上述的三项任务:

  • schema:输出Schema,用来验证工具函数返回的结果是否满足既定的结构;
  • render:如何将作为工具结果的JSON转换成能够被LLM识别的标准消息内容(ContentBlock列表);
  • presentationMeta:将结果转换成利于呈现的Json。

1.2 ToolRunContext

ToolDefinition利用其execute方法执行工具承载的业务逻辑。args参数代表执行工具的参数,一般由LLM提供。exec参数对应的ToolRunContext接口是对工具执行上下文的描述,该接口派生于ToolExecution接口,后者又是对ToolExecutionInput接口的扩展。顾名思义,ToolExecutionInput接口表示工具执行的输入,具体的输入是根据LLM返回的assistent消息携带的某个ToolCall生成的。

exportinterfaceToolExecutionInput{readonlycallId:ToolCallIdreadonlyrootCallId?:ToolCallIdreadonlyname:stringreadonlyarguments:unknownreadonlyagent?:Agentreadonlyparent?:ToolExecutionTokenreadonlysignal:AbortSignal}exporttypeToolCallId=Branded<'ToolCallId'>exporttypeToolExecutionToken=symbol&{readonly[toolExecutionTokenBrand]:true}declareconsttoolExecutionTokenBrand:uniquesymbol

描述工具输入的各个成员说明如下:

  • callId:工具调用的唯一标识;
  • rootCallId和parent: 工具调用存在类似于调用栈或者调用链的概念,比如针对SubAgent的调用,或者当前工具以PTC(Programmatic Tool Calling,DSH针对CodeAct的实现)模式被调用,此时rootCallId用来指向最外层的工具调用,parent返回的ToolExecutionToken(一个Symbol)指向父节点;
  • name:工具名称;
  • arguments:传入工具函数的参数;
  • agent: 当前的Agent;
  • signal:利用这个AbortSignal对象得到调用者的中止意图。

派生于ToolExecutionInput的ToolExecution接口描述的是注册中心管道里的一次待执行工具调用。它是ToolExecutionInput的成品版,经过注册中心加工后,所有关键身份和参数都已冻结变成只读。对于ToolExecutionInput来说,其rootCallId字段作为输入是可以缺省的,但是作为一个描述工具执行的对象来说,则是必需的成员。顺便说一些,这里的继承好没道理,Execution怎么能继承Input呢?难道不应该使用组合代替继承吗?ToolExecutionInput的parent指向的就是ToolExecution的token。

exportinterfaceToolExecutionextendsToolExecutionInput{readonlyrootCallId:ToolCallIdreadonlytoken:ToolExecutionToken}

ToolRunContext接口在ToolExecution基础上定了额外两个方法:deferContext方法的作用是在对话历史位于当前工具结果消息之后,添加一个用户消息(角色为user)作为上下文,concludeTurn方法则直接向Agent Loop发送结束此轮Agent调用的信号。

exportinterfaceToolRunContextextendsToolExecution{deferContext(context:UserMessage):voidconcludeTurn():void}

2. defineTool函数

我们一般会利用如下这个泛型的defineTool函数来根据指定的DefineToolOptions<S, O>对象来创建工具注册。泛型参数S和O分别表示针对针对参数和输出的Schema规范,是分别对ParameterSchemaSpec和ValueSchemaSpec接口的扩展。

exportfunctiondefineTool<constSextendsParameterSchemaSpec,constOextendsValueSchemaSpec>(options:DefineToolOptions<S,O>,):ToolDefinition

2.1 ValueSchemaSpec

ValueSchemaSpec描述的是单值对象的Schema。由于Schema由对应的类型决定,ValueSchemaSpec类型是基于type字段的一组可判别联合类型(discriminated union)。针对字符串、数字、整数、布尔值、Null、数组、对象和Json对应的类型定义如下,OneOfValueSchemaSpec表达的意思是在给出的一组多于两个的ValueSchemaSpec选项中,满足其中一项。它们都利用扩展的ValueSchemaAnnotations接口拥有共同的字段:description、title、default和examples。

exporttypeValueSchemaSpec=|StringValueSchemaSpec|NumberValueSchemaSpec|IntegerValueSchemaSpec|BooleanValueSchemaSpec|NullValueSchemaSpec|ArrayValueSchemaSpec|ObjectValueSchemaSpec|JsonValueSchemaSpec|OneOfValueSchemaSpecexportinterfaceStringValueSchemaSpecextendsValueSchemaAnnotations{type:'string'enum?:readonlystring[]const?:string}exportinterfaceNumberValueSchemaSpecextendsValueSchemaAnnotations{type:'number'enum?:readonlynumber[]const?:number}exportinterfaceIntegerValueSchemaSpecextendsValueSchemaAnnotations{type:'integer'enum?:readonlynumber[]const?:number}exportinterfaceBooleanValueSchemaSpecextendsValueSchemaAnnotations{type:'boolean'enum?:readonlyboolean[]const?:boolean}exportinterfaceNullValueSchemaSpecextendsValueSchemaAnnotations{type:'null'enum?:readonlynull[]const?:null}exportinterfaceArrayValueSchemaSpecextendsValueSchemaAnnotations{type:'array'items?:ValueSchemaSpec}exportinterfaceObjectValueSchemaSpecextendsValueSchemaAnnotations{type:'object'properties?:ParameterSchemaSpec additionalProperties:boolean}exportinterfaceJsonValueSchemaSpecextendsValueSchemaAnnotations{type:'json'}exportinterfaceOneOfValueSchemaSpecextendsValueSchemaAnnotations{oneOf:readonly[ValueSchemaSpec,ValueSchemaSpec,...ValueSchemaSpec[]]}exportinterfaceValueSchemaAnnotations{description?:stringtitle?:stringdefault?:JsonValue examples?:JsonValue}

2.2 ParameterPropertySpec

表示参数Schema规范,本质上是一个将代表参数名称作为Key,代表对应参数Schema规范的ParameterPropertySpec作为Value的字典,并利用[key: symbol]: never不允许任何Symbol作为Key。ParameterPropertySpec则是在ValueSchemaSpec解除添加一个表示参数是否必须提供的required字段(默认为true)。

exporttypeParameterSchemaSpec={[key:string]:ParameterPropertySpec[key:symbol]:never}exporttypeParameterPropertySpec=ValueSchemaSpec&{required?:true}

2.3 DefineToolOptions

defineTool函数最终使用符合如下这个DefineToolOptions<S,O>接口结构的对象来创建作为工具定义的ToolDefinition,所以我们可以看出两个接口的定义基本上是一一对应的。不同之处在于通过通过对派生于ParameterSchemaSpec和ValueSchemaSpec的两个泛型参数的引入,让我们在定义参数和返回值的Schema时,可能更加明确地根据类型进行对应的Schema设置。

exportinterfaceDefineToolOptions<SextendsParameterSchemaSpec,OextendsValueSchemaSpec>{readonlyname:stringreadonlydescription:stringreadonlyparameters:Sreadonlyoutput:{readonlyschema:Orender(args:InferArgs<S>,value:InferValue<NoInfer<O>>):ContentBlock[]presentationMeta?(args:InferArgs<S>,value:InferValue<NoInfer<O>>):JsonValue}readonlytimeoutMs?:numberisConcurrencySafe?(args:InferArgs<S>):booleanexecute(args:InferArgs<S>,exec:ToolRunContext):Promise<InferValue<NoInfer<O>>>finalizeContent?(exec:Readonly<ToolExecution>,result:Readonly<ToolExecutionResult>):ContentBlock[]|undefinedpresentCall?(args:InferArgs<S>):ToolCallView|undefinedpresentResult?(args:InferArgs<S>,result:ToolResult):ToolResultView|undefined}

3. 工具约束与执行前的防御

DeepSeek Harness深度拆解-10:工具如何定义和注册除了演示如何定义并注册工具外,还分别演示了通过调用ctx.tools的restrict来注册工具约束,以及通过调用guard方法来注册一个工具之前会执行的回调。

3.1 ToolRestriction

前者注册的工具约束通过如下这个ToolRestriction接口表示。它本质上利用allow和deny两个字段分别提供了基于工具名称的白名单和黑名单。

exportinterfaceToolRestriction{readonlyallow?:readonlystring[]readonlydeny?:readonlystring[]}exportclassToolRuntimeextendsService{restrict(filter:ToolRestriction):()=>void}

不论是工具定义,还是工具约束和工具守卫,都是按照Scope进行注册的,Scope在DSH中是非常重要的概念,我的文章DeepSeek Harness深度拆解-09:基于Scope的隔离机制是如何实现的?具有对此的详细介绍。这样设计使我们可以为专属的Agent注册工具,而不是只能注册全局可用的工具,出现工具爆炸的问题。注册的ToolRestriction本质上是对工具的过滤,或者控制工具的可见性(是否需要传递给LLM),但是它过滤的对象是全局工具,不是Scope自己注册的工具。而且restrict方法不能在没有Scope绑定的Context中执行,换言之只能基于某个Scope(比如Agent)定义过滤规则,不能定义一个全局的过滤规则。

3.2 ToolGuard

通过调用ctx.tools的guard方法注册的是一个ToolGuard类型。如代码所示,这是一个将ToolExecution作为输入,返回string | undefined的函数。正如其命名所示的那样,注册的这个回调函数就是为工具执行设置的门卫,它会在工具函数被调用之前对工具的可执行性做一次检查。如果回调函数放行,会直接返回undefined,否则会返回一个描述拒绝执行的文本作为返回值。

exporttypeToolGuard=(execution:Readonly<ToolExecution>)=>string|undefinedexportclassToolRuntimeextendsService{guard(guard:ToolGuard):()=>void}

4. 基于Scope的工具注册

提供给LLM的候选工具的差异会对LLM做出的推理质量产生很大的影响,所以我们不能无脑地将全局注册的工具发给LLM。DSH针对工具的注册采用基于Scope的分层策略,原则上我们可用按照我们定义的任意形式的逻辑层次结构来定义Scope,比如正对Agent来定义Scope就是一种场景的方式,下面的程序针对这种注册方式的演示:

import{Context}from'@deepseek-ai/cordis'import{createScope}from'@deepseek-ai/dsh-scope'import{ToolRuntime,defineTool}from'@deepseek-ai/dsh-tools'import{SystemPrompt}from'@deepseek-ai/dsh-system-prompt'constctx=newContext()ctx.plugin(ToolRuntime)ctx.plugin(SystemPrompt)functionregisterTool(ctx:Context,name:string):()=>void{returnctx.tools.register(defineTool({name:name,description:`test tool${name}`,parameters:{},output:{schema:{type:'string'},render:(_args,value:string)=>[{type:'text',text:value}],},asyncexecute(args:any,_){return`${args}`}}))}constfiber=ctx.inject(["tools"],(ctx)=>{constagent1ScopeKey={id:"agent1"},agent2ScopeKey={id:"agent2"}constagent1=createScope(ctx,agent1ScopeKey),agent2=createScope(ctx,agent2ScopeKey)registerTool(ctx,"foo")registerTool(agent1.ctx,"bar")registerTool(agent2.ctx,"baz")console.log(ctx.tools.schemas(agent1ScopeKey))console.log(ctx.tools.schemas(agent2ScopeKey))})awaitfiber.await()

如代码所示,我们直接调用根Context的plugin方法将ToolRuntime服务以插件的方式注册。由于ToolRuntime依赖于SystemPrompt提供的基于构建系统提示词的服务,所以我们需要一并注册。接下来在一个通过调用inject方法(利用它注入依赖服务tools)注册的插件函数中,我们模拟针对两个Agent(agent1和agent2)构建了对应的Scope,然后分别调用registerTool函数在根Context和这两个Scope对应的Context上注册了三个工具,分别命名为foo、bar和baz。我们最后分别以两个Scope的Key作为参数调用ctx.tools的schemas方法,它会返回最终发送给LLM的候选工具Schema。输出的两段JSON如下所示:

[{name:'foo',description:'test tool foo',parameters:{type:'object',properties:{}}},{name:'bar',description:'test tool bar',parameters:{type:'object',properties:{}}}]
[{name:'foo',description:'test tool foo',parameters:{type:'object',properties:{}}},{name:'baz',description:'test tool baz',parameters:{type:'object',properties:{}}}]

可以看出注册在根Context上的工具foo会被所有Agent共享,在针对Agent对应Scope中注册的工具bar和baz则专属于对应的Agent。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询