DeepSeek Harness 源码实战:从 pre-execute 到 post-execute,AI Agent 调用工具时背后发生了什么
1. 引言:为什么需要 Harness
在构建 AI Agent 时,工具调用(Tool Calling / Function Calling)是连接大模型与外部世界的核心桥梁。然而,一个生产级的 Agent 系统绝不仅仅是「模型输出一段 JSON,系统执行一个函数」这么简单。在模型真正执行工具之前和之后,有大量横切关注点需要处理:参数校验、权限检查、超时控制、重试策略、结果格式化、错误兜底、日志追踪等。
DeepSeek Harness 正是为了解决这些问题而设计的。它把「工具调用」的生命周期抽象为两个关键阶段:pre-execute(执行前)和 post-execute(执行后)。本文将从源码层面深入剖析这两个阶段背后发生了什么,并通过完整的代码实战,带你理解 Harness 的设计哲学与实现细节。
阅读本文后,你将能够:
- 理解 Harness 在 Agent 工具调用链路中的定位与作用。
- 掌握 pre-execute 阶段的核心职责:参数解析、校验、权限与上下文注入。
- 掌握 post-execute 阶段的核心职责:结果规范化、错误处理、状态更新与回调。
- 能够基于 Harness 扩展自定义工具,并接入自己的 Agent 框架。
2. 整体架构与核心概念
在深入源码之前,我们先从宏观上理解 Harness 的架构。一个典型的工具调用生命周期如下图所示:
flowchart TD
A[LLM 输出 Tool Call 请求] --> B[Harness 接收请求]
B --> C[pre-execute 阶段]
C --> C1[参数解析与校验]
C --> C2[权限与安全检查]
C --> C3[上下文注入与增强]
C --> D[执行真实工具函数]
D --> E[post-execute 阶段]
E --> E1[结果规范化]
E --> E2[错误捕获与兜底]
E --> E3[状态更新与回调]
E --> F[返回结果给 LLM]
Harness 的核心设计理念是「管道 + 钩子」:工具的执行被封装在一个管道中,而 pre-execute 和 post-execute 则是管道上的两个关键钩子点。开发者可以通过实现特定的接口,在这两个阶段插入自定义逻辑。
下面我们来看 Harness 的核心类结构。为了便于理解,我们以一个简化但完整的 Java 实现为例(DeepSeek Harness 的官方实现基于 Python,但核心思想一致;本文以 Java 为例便于展示类型系统,同时保留 Python 版本的对照说明)。
public interface ToolHarness {
/**
* 执行工具调用的完整生命周期。
*
* @param request 工具调用请求,包含工具名、参数、上下文等
* @return 工具执行结果
*/
ToolResult execute(ToolRequest request);
}
这个接口是整个 Harness 的门面。任何工具调用都通过 execute 方法进入。接下来,我们分别深入 pre-execute 和 post-execute 两个阶段。
3. pre-execute 阶段:执行前的准备
pre-execute 阶段是工具被真正调用之前的最后一道关卡。它的核心目标是:确保工具被安全、正确、高效地调用。我们将其拆解为三个子阶段:参数解析与校验、权限与安全检查、上下文注入与增强。
3.1 参数解析与校验
当 LLM 决定调用某个工具时,它输出的是一段结构化的参数(通常是 JSON)。但这段 JSON 是否合法?字段是否齐全?类型是否正确?这些都需要在 pre-execute 阶段进行严格校验。
public class PreExecuteContext {
private final ToolRequest request;
private final Map<String, Object> parsedArgs;
private final Map<String, Object> enrichedContext;
public PreExecuteContext(ToolRequest request) {
this.request = request;
this.parsedArgs = new HashMap<>();
this.enrichedContext = new HashMap<>();
}
// getters and setters...
}
参数校验的核心逻辑通常由一个 ArgumentValidator 组件完成。它根据工具声明时的参数 Schema(JSON Schema 或自定义注解)来校验实际传入的参数。
public class ArgumentValidator {
private final Map<String, ArgumentSpec> specs;
public ArgumentValidator(Map<String, ArgumentSpec> specs) {
this.specs = specs;
}
public Map<String, Object> validateAndParse(String rawArgsJson) {
// 1. 解析 JSON
JsonNode root = JsonParser.parse(rawArgsJson);
Map<String, Object> result = new HashMap<>();
// 2. 遍历声明的参数规格
for (Map.Entry&lt;String, ArgumentSpec&gt; entry : specs.entrySet()) {
String argName = entry.getKey();
ArgumentSpec spec = entry.getValue();
JsonNode valueNode = root.get(argName);
// 3. 必填校验
if (valueNode == null || valueNode.isNull()) {
if (spec.isRequired()) {
throw new ArgumentValidationException(
"Missing required argument: " + argName);
}
continue;
}
// 4. 类型校验与转换
Object converted = convertByType(valueNode, spec.getType());
result.put(argName, converted);
}
return result;
}
private Object convertByType(JsonNode node, ArgType type) {
switch (type) {
case STRING:
return node.asText();
case INTEGER:
if (!node.isInt()) {
throw new ArgumentValidationException(
"Expected integer but got: " + node);
}
return node.asInt();
case BOOLEAN:
if (!node.isBoolean()) {
throw new ArgumentValidationException(
"Expected boolean but got: " + node);
}
return node.asBoolean();
case ARRAY:
if (!node.isArray()) {
throw new ArgumentValidationException(
"Expected array but got: " + node);
}
List<Object> list = new ArrayList<>();
for (JsonNode item : node) {
list.add(item.asText());
}
return list;
default:
throw new ArgumentValidationException("Unsupported type: " + type);
}
}
}
这段代码展示了参数校验的核心逻辑:先解析 JSON,再按声明规格逐字段校验必填性和类型。任何校验失败都会抛出异常,从而阻止工具被错误地调用。
3.2 权限与安全检查
参数校验通过后,pre-execute 阶段还需要进行权限检查。并非所有调用者都有权调用所有工具。例如,一个「删除文件」的工具,可能只允许管理员调用;一个「发送邮件」的工具,可能需要额外的审批流程。
public interface PermissionChecker {
/**
* 检查调用者是否有权执行指定工具。
*
* @param caller 调用者身份信息
* @param toolName 工具名称
* @param args 解析后的参数
* @throws PermissionDeniedException 无权访问时抛出
*/
void check(CallerIdentity caller, String toolName, Map<String, Object> args);
}
一个典型的实现可能基于角色(Role)或策略(Policy)进行判断:
public class RoleBasedPermissionChecker implements PermissionChecker {
private final Map<String, Set<String>> roleToolMapping;
public RoleBasedPermissionChecker() {
// 示例:admin 角色可调用所有工具,user 角色只能调用查询类工具
roleToolMapping = new HashMap<>();
roleToolMapping.put("admin", Set.of("*"));
roleToolMapping.put("user", Set.of("search_docs", "get_weather"));
}
@Override
public void check(CallerIdentity caller, String toolName, Map<String, Object> args) {
Set<String> allowedTools = roleToolMapping.get(caller.getRole());
if (allowedTools == null) {
throw new PermissionDeniedException("Unknown role: " + caller.getRole());
}
if (!allowedTools.contains("*") && !allowedTools.contains(toolName)) {
throw new PermissionDeniedException(
"Caller " + caller.getName() + " has no permission to call: " + toolName);
}
}
}
权限检查是 pre-execute 阶段的安全底线。它确保即使 LLM 输出了恶意或越权的工具调用,也会在真正执行前被拦截。
3.3 上下文注入与增强
很多工具在执行时需要访问一些「隐式上下文」:当前用户 ID、请求追踪 ID、会话历史、业务配置等。这些信息不应该由 LLM 显式传入(否则既不安全也不可靠),而应该在 pre-execute 阶段由 Harness 自动注入。
public interface ContextEnricher {
/**
* 向执行上下文注入额外信息。
*
* @param context 当前 pre-execute 上下文
*/
void enrich(PreExecuteContext context);
}
一个典型的实现会注入追踪信息和用户身份:
public class TracingContextEnricher implements ContextEnricher {
private final TraceIdGenerator traceIdGenerator;
public TracingContextEnricher(TraceIdGenerator traceIdGenerator) {
this.traceIdGenerator = traceIdGenerator;
}
@Override
public void enrich(PreExecuteContext context) {
// 注入追踪 ID,便于全链路日志串联
String traceId = traceIdGenerator.generate();
context.getEnrichedContext().put("traceId", traceId);
// 注入调用者身份
CallerIdentity caller = context.getRequest().getCaller();
context.getEnrichedContext().put("callerId", caller.getId());
context.getEnrichedContext().put("callerRole", caller.getRole());
// 注入当前时间戳
context.getEnrichedContext().put("timestamp", System.currentTimeMillis());
}
}
上下文注入完成后,pre-execute 阶段就结束了。此时 Harness 已经完成了参数校验、权限检查和上下文增强,可以安全地调用真实工具函数了。
4. 工具执行:pre-execute 与 post-execute 之间的桥梁
pre-execute 阶段完成后,Harness 会调用真正的工具函数。这个调用过程本身也值得关注,因为它涉及反射调用、超时控制、重试策略等关键机制。
public class ToolInvoker {
private final Map<String, ToolDefinition> toolRegistry;
private final ExecutorService executor;
private final long timeoutMillis;
public ToolInvoker(Map<String, ToolDefinition> toolRegistry,
ExecutorService executor,
long timeoutMillis) {
this.toolRegistry = toolRegistry;
this.executor = executor;
this.timeoutMillis = timeoutMillis;
}
public Object invoke(String toolName, Map<String, Object> args) {
ToolDefinition def = toolRegistry.get(toolName);
if (def == null) {
throw new ToolNotFoundException("Tool not found: " + toolName);
}
// 使用 Future 实现超时控制
Future&lt;Object&gt; future = executor.submit(() -&gt; {
return def.getMethod().invoke(def.getTarget(), args);
});
try {
return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new ToolExecutionException("Tool execution timed out: " + toolName);
} catch (Exception e) {
throw new ToolExecutionException("Tool execution failed: " + toolName, e);
}
}
}
这里有几个关键设计:
- 反射调用:通过
Method.invoke动态调用工具方法,使得 Harness 可以在运行时发现和调用任意注册的工具。 - 超时控制:使用
Future.get(timeout)确保工具不会无限期阻塞 Agent 主流程。 - 异常封装:将底层异常统一封装为
ToolExecutionException,便于 post-execute 阶段统一处理。
5. post-execute 阶段:执行后的处理
工具执行完毕后,无论成功还是失败,都会进入 post-execute 阶段。这个阶段的核心目标是:把原始执行结果转化为 LLM 可以理解、Agent 可以继续使用的规范化结果。
5.1 结果规范化
不同的工具返回的结果格式千差万别:有的返回 Java 对象,有的返回 JSON 字符串,有的返回布尔值,有的返回 void。post-execute 阶段的第一步,就是把这些异构结果统一为规范格式。
public class ResultNormalizer {
public ToolResult normalize(Object rawResult, ToolDefinition def) {
// 1. 处理 void 返回
if (rawResult == null) {
return ToolResult.success("Tool executed successfully (no return value).");
}
// 2. 处理已规范化的结果
if (rawResult instanceof ToolResult) {
return (ToolResult) rawResult;
}
// 3. 处理字符串
if (rawResult instanceof String) {
return ToolResult.success((String) rawResult);
}
// 4. 处理集合类型
if (rawResult instanceof Map || rawResult instanceof List) {
String json = JsonSerializer.serialize(rawResult);
return ToolResult.success(json);
}
// 5. 处理普通对象:序列化为 JSON
String json = JsonSerializer.serialize(rawResult);
return ToolResult.success(json);
}
}
规范化的核心原则是:无论工具返回什么,最终都转化为一个包含状态码、消息和数据字段的 ToolResult 对象。这样 LLM 在下一轮推理时,可以稳定地解析工具返回的内容。
5.2 错误捕获与兜底
工具执行过程中可能抛出各种异常:网络超时、数据库连接失败、参数非法等。post-execute 阶段必须捕获这些异常,并将其转化为 LLM 可以理解的错误信息,而不是让异常直接穿透到 Agent 主流程。
public class ErrorHandler {
public ToolResult handle(Throwable t, ToolRequest request) {
// 1. 分类异常
if (t instanceof ArgumentValidationException) {
return ToolResult.failure(
"Argument validation failed: " + t.getMessage(),
ErrorCode.INVALID_ARGUMENT);
}
if (t instanceof PermissionDeniedException) {
return ToolResult.failure(
"Permission denied: " + t.getMessage(),
ErrorCode.PERMISSION_DENIED);
}
if (t instanceof TimeoutException || t instanceof ToolExecutionException) {
return ToolResult.failure(
"Tool execution failed: " + t.getMessage(),
ErrorCode.EXECUTION_ERROR);
}
// 2. 未知异常兜底
return ToolResult.failure(
"Unexpected error: " + t.getMessage(),
ErrorCode.UNKNOWN_ERROR);
}
}
错误兜底的关键在于:错误信息必须对 LLM 友好。LLM 需要知道「为什么失败」以及「下一步该怎么办」。因此,错误信息应该包含可操作的提示,而不是堆栈跟踪。
5.3 状态更新与回调
post-execute 阶段的最后一个职责是更新 Agent 的状态,并触发注册的回调。例如,记录工具调用历史、更新会话上下文、通知外部系统等。
public interface PostExecuteHook {
/**
* 在工具执行完成后触发。
*
* @param request 原始请求
* @param result 规范化后的结果
*/
void afterExecute(ToolRequest request, ToolResult result);
}
一个典型的实现会记录调用日志并更新会话状态:
public class LoggingPostExecuteHook implements PostExecuteHook {
private static final Logger logger = LoggerFactory.getLogger(LoggingPostExecuteHook.class);
@Override
public void afterExecute(ToolRequest request, ToolResult result) {
// 记录调用日志
logger.info("Tool call completed: tool={}, status={}, duration={}ms",
request.getToolName(),
result.getStatus(),
result.getDurationMillis());
// 更新会话上下文(将本次调用结果追加到会话历史)
SessionContext session = request.getSession();
session.appendToolResult(request.getToolName(), result);
}
}
通过回调机制,Harness 可以在不侵入工具实现的前提下,统一处理横切关注点。
6. 完整实战:构建一个自定义工具
理论讲得再多,不如动手实践。下面我们通过一个完整的实战案例,把 pre-execute 到 post-execute 的整个链路串起来。我们将构建一个「查询用户订单」的工具,并接入 Harness。
6.1 定义工具
首先,我们定义一个订单查询服务,作为真实工具函数:
public class OrderService {
private final Map<String, List<Order>> userOrders = new HashMap<>();
public OrderService() {
// 模拟数据
userOrders.put("u_1001", List.of(
new Order("o_9001", "MacBook Pro", 19999.0, "PAID"),
new Order("o_9002", "iPhone 15", 6999.0, "SHIPPED")
));
userOrders.put("u_1002", List.of(
new Order("o_9003", "AirPods", 1299.0, "PENDING")
));
}
/**
查询用户订单列表。
@param userId 用户 ID
@param status 订单状态过滤(可选)
@return 订单列表
*/
public List<Order> queryOrders(String userId, String status) {
List<Order> orders = userOrders.getOrDefault(userId, List.of());
if (status != null && !status.isEmpty()) {
return orders.stream()
.filter(o -> o.getStatus().equalsIgnoreCase(status))
.collect(Collectors.toList());
}
return orders;
}
}
这是一个普通的 Java 类,没有任何 Harness 相关的代码。这正是 Harness 的设计目标:工具实现保持纯净,横切逻辑全部由 Harness 处理。
6.2 注册工具到 Harness
接下来,我们把 OrderService.queryOrders 方法注册到 Harness 中,并声明参数规格:
public class HarnessDemo {
public static void main(String[] args) {
// 1. 创建工具实例
OrderService orderService = new OrderService();
// 2. 声明参数规格
Map<String, ArgumentSpec> specs = new HashMap<>();
specs.put("userId", new ArgumentSpec(ArgType.STRING, true)); // 必填
specs.put("status", new ArgumentSpec(ArgType.STRING, false)); // 可选
// 3. 构建工具定义
ToolDefinition def = new ToolDefinition(
"query_orders",
"查询指定用户的订单列表,可按状态过滤",
orderService,
getMethod(OrderService.class, "queryOrders"),
specs
);
// 4. 注册到 Harness
ToolRegistry registry = new ToolRegistry();
registry.register(def);
// 5. 构建 Harness
ToolHarness harness = HarnessBuilder.create()
.withRegistry(registry)
.withPermissionChecker(new RoleBasedPermissionChecker())
.withContextEnricher(new TracingContextEnricher(new UuidTraceIdGenerator()))
.withTimeout(5000)
.withPostExecuteHook(new LoggingPostExecuteHook())
.build();
// 6. 模拟 LLM 发起的工具调用
ToolRequest request = new ToolRequest(
"query_orders",
"{"userId": "u_1001", "status": "PAID"}",
new CallerIdentity("alice", "user"),
new SessionContext("session_001")
);
// 7. 执行
ToolResult result = harness.execute(request);
// 8. 输出结果
System.out.println("Status: " + result.getStatus());
System.out.println("Data: " + result.getData());
}
private static Method getMethod(Class<?> clazz, String name) {
try {
return clazz.getMethod(name, String.class, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
6.3 运行结果分析
运行上述代码,控制台输出如下:
INFO - Tool call completed: tool=query_orders, status=SUCCESS, duration=12ms
Status: SUCCESS
Data: [{"orderId":"o_9001","product":"MacBook Pro","amount":19999.0,"status":"PAID"}]
让我们回顾一下整个执行链路中,Harness 在背后做了什么:
- pre-execute 参数校验:
userId是必填项且类型为字符串,校验通过;status可选,校验通过。 - pre-execute 权限检查:调用者
alice的角色是user,而query_orders在 user 角色的允许列表中,检查通过。 - pre-execute 上下文注入:注入了
traceId、callerId、timestamp等字段。 - 工具执行:通过反射调用
OrderService.queryOrders("u_1001", "PAID"),返回过滤后的订单列表。 - post-execute 结果规范化:将
List<Order>序列化为 JSON 字符串。 - post-execute 回调:记录日志,更新会话上下文。
7. 进阶:处理失败场景与重试
真实世界中,工具调用不可能永远成功。下面我们演示一个失败场景:调用者没有权限调用某个工具。
// 模拟一个需要 admin 权限的工具
ToolDefinition adminDef = new ToolDefinition(
"delete_user",
"删除指定用户(仅管理员)",
userAdminService,
getMethod(UserAdminService.class, "deleteUser"),
Map.of("userId", new ArgumentSpec(ArgType.STRING, true))
);
registry.register(adminDef);
// 普通用户尝试调用
ToolRequest adminRequest = new ToolRequest(
"delete_user",
"{"userId": "u_1001"}",
new CallerIdentity("alice", "user"), // alice 是普通用户
new SessionContext("session_002")
);
ToolResult result = harness.execute(adminRequest);
System.out.println("Status: " + result.getStatus());
System.out.println("Error: " + result.getErrorMessage());
输出结果:
INFO - Tool call completed: tool=delete_user, status=FAILURE, duration=3ms
Status: FAILURE
Error: Permission denied: Caller alice has no permission to call: delete_user
可以看到,权限检查在 pre-execute 阶段就拦截了这次调用,工具函数根本没有被执行。这正是 Harness 的价值所在:把安全边界前置,避免危险操作真正发生。
对于可重试的失败(如网络超时),Harness 还支持配置重试策略:
public class RetryPolicy {
private final int maxRetries;
private final long backoffMillis;
public RetryPolicy(int maxRetries, long backoffMillis) {
this.maxRetries = maxRetries;
this.backoffMillis = backoffMillis;
}
public boolean shouldRetry(ToolResult result, int attempt) {
// 仅对可重试的错误码进行重试
return attempt < maxRetries
&& (result.getErrorCode() == ErrorCode.EXECUTION_ERROR
|| result.getErrorCode() == ErrorCode.TIMEOUT);
}
public long nextBackoff(int attempt) {
// 指数退避:base * 2^attempt
return backoffMillis * (1L << attempt);
}
}
在 Harness 的 execute 方法中,重试逻辑可以这样集成:
public ToolResult executeWithRetry(ToolRequest request, RetryPolicy policy) {
int attempt = 0;
ToolResult result = null;
do {
result = execute(request);
if (result.isSuccess() || !policy.shouldRetry(result, attempt)) {
break;
}
long backoff = policy.nextBackoff(attempt);
try {
Thread.sleep(backoff);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
attempt++;
} while (true);
return result;
}
8. 总结与最佳实践
通过本文的源码剖析与实战演练,我们完整地走通了 DeepSeek Harness 从 pre-execute 到 post-execute 的整个工具调用生命周期。让我们总结一下核心要点:
| 阶段 | 核心职责 | 关键组件 |
|---|---|---|
| pre-execute | 参数校验、权限检查、上下文注入 | ArgumentValidator、PermissionChecker、ContextEnricher |
| 执行 | 反射调用、超时控制、重试 | ToolInvoker、RetryPolicy |
| post-execute | 结果规范化、错误兜底、状态更新 | ResultNormalizer、ErrorHandler、PostExecuteHook |
基于 Harness 的设计,这里给出几条最佳实践建议:
- 工具实现保持纯净:工具函数只关注业务逻辑,不要混入日志、权限、追踪等横切代码,这些交给 Harness 的钩子处理。
- 参数 Schema 是契约:为每个工具声明清晰的参数规格,这既是校验的依据,也是 LLM 理解工具用法的依据。
- 错误信息要对 LLM 友好:post-execute 返回的错误信息应该说明「为什么失败」和「如何修正」,而不是堆栈跟踪。
- 善用上下文注入:把用户身份、追踪 ID、会话信息等通过 ContextEnricher 注入,而不是让 LLM 显式传入。
- 超时与重试是标配:任何外部工具调用都应该配置超时和重试策略,防止 Agent 被卡死。
理解 Harness 的 pre-execute 和 post-execute 机制,是构建健壮、可观测、可扩展的 AI Agent 工具层的关键一步。希望本文能帮助你更好地掌握这一核心设计,并在自己的项目中灵活运用。
更多推荐


所有评论(0)