跳到主要内容

Tool-Using Agent TypeScript 模板

这篇文档只做一件事:

把 Tool-Using Agent 的最小工程骨架整理成一份可以直接复制的 TypeScript 模板。

如果已经看过:

但真正自己动手时还是会卡在这些地方:

  • 多个工具的 schema 怎么定义
  • planner 输出应该长什么样
  • facts / evidence / toolHistory 该怎么放进 state
  • 工具失败以后,到底是重试、换工具,还是结束
  • 第一版模板应该保留哪些最小边界

这一页就是给你直接复制、再按需替换的。

1. 适合什么时候用这个模板

这个模板适合下面这类系统:

  • 客户跟进判断助手
  • 内部支持助手
  • 订单 / 工单分析助手
  • 数据查询后再给建议的业务助手

它比较适合下面这种状态:

  • 已经不满足于单个工具的最小 Agent
  • 需要在多个工具之间做选择
  • 希望系统能围绕目标持续决定下一步
  • 想先做一个轻量版本,不想一开始就接重框架

如果只是做:

  • 单轮问答
  • 一次性工具调用
  • 不需要在多个工具之间选择的简单流程

那最小 Agent 模板往往已经够用了。

2. 完整 TypeScript 模板代码

下面这份模板刻意保持为:

单文件、最小依赖、完整主线。

可以先整段复制,再替换后面标出来的几个位置。

// tool-using-agent-template.ts

type ToolArgs = Record<string, unknown>;

type ToolResult<T = unknown> = {
ok: boolean;
data?: T;
error?: string;
};

type ToolDefinition<TArgs = ToolArgs, TResult = unknown> = {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<string, unknown>;
required?: string[];
};
run: (args: TArgs) => Promise<ToolResult<TResult>>;
};

type EvidenceItem = {
source: string;
summary: string;
confidence: number;
};

type ToolCallRecord = {
step: number;
toolName: string;
args: ToolArgs;
resultSummary: string;
success: boolean;
durationMs: number;
};

type AgentState = {
userGoal: string;
currentStep: number;
maxSteps: number;
status: "running" | "completed" | "failed";
facts: Record<string, unknown>;
evidence: EvidenceItem[];
toolHistory: ToolCallRecord[];
pendingQuestions: string[];
finalAnswer: string | null;
lastError?: string;
};

type AgentDecision =
| {
type: "tool";
toolName: string;
args: ToolArgs;
reason: string;
}
| {
type: "finish";
reason: string;
answer: string;
};

function createInitialState(userGoal: string): AgentState {
return {
userGoal,
currentStep: 0,
maxSteps: 6,
status: "running",
facts: {},
evidence: [],
toolHistory: [],
pendingQuestions: [],
finalAnswer: null,
};
}

const getUserActivityTool: ToolDefinition<
{ userId: string; days: number },
{ userId: string; activeDays: number; lastActiveAt: string | null }
> = {
name: "get_user_activity",
description: "Get recent activity summary for a target user",
inputSchema: {
type: "object",
properties: {
userId: { type: "string" },
days: { type: "number" },
},
required: ["userId", "days"],
},
async run(args) {
return {
ok: true,
data: {
userId: args.userId,
activeDays: 1,
lastActiveAt: "2026-04-27T10:20:00Z",
},
};
},
};

const getOrderStatusTool: ToolDefinition<
{ userId: string },
{ userId: string; hasRecentOrder: boolean; lastOrderStatus: string }
> = {
name: "get_order_status",
description: "Get order or payment status for a target user",
inputSchema: {
type: "object",
properties: {
userId: { type: "string" },
},
required: ["userId"],
},
async run(args) {
return {
ok: true,
data: {
userId: args.userId,
hasRecentOrder: false,
lastOrderStatus: "refunded",
},
};
},
};

const getTicketStatusTool: ToolDefinition<
{ userId: string },
{ userId: string; hasOpenTicket: boolean; ticketCount: number }
> = {
name: "get_ticket_status",
description: "Get recent support ticket information for a target user",
inputSchema: {
type: "object",
properties: {
userId: { type: "string" },
},
required: ["userId"],
},
async run(args) {
return {
ok: true,
data: {
userId: args.userId,
hasOpenTicket: true,
ticketCount: 2,
},
};
},
};

const tools: ToolDefinition[] = [
getUserActivityTool,
getOrderStatusTool,
getTicketStatusTool,
];

function getToolByName(name: string): ToolDefinition | undefined {
return tools.find((tool) => tool.name === name);
}

function logStep(message: string, extra?: unknown) {
const now = new Date().toISOString();
if (extra === undefined) {
console.log(`[${now}] ${message}`);
return;
}
console.log(`[${now}] ${message}`, extra);
}

async function decideNextAction(state: AgentState): Promise<AgentDecision> {
if (!state.facts.userId) {
return {
type: "tool",
toolName: "get_user_activity",
args: { userId: "user_123", days: 7 },
reason: "Need recent activity before making follow-up judgment",
};
}

if (state.facts.activeDays !== undefined && state.facts.hasRecentOrder === undefined) {
return {
type: "tool",
toolName: "get_order_status",
args: { userId: String(state.facts.userId) },
reason: "Need order status to understand business value context",
};
}

if (state.facts.hasOpenTicket === undefined) {
return {
type: "tool",
toolName: "get_ticket_status",
args: { userId: String(state.facts.userId) },
reason: "Need support signal before final recommendation",
};
}

return {
type: "finish",
reason: "Enough evidence collected for a first recommendation",
answer: buildFinalAnswer(state),
};
}

function updateFactsFromToolResult(
state: AgentState,
toolName: string,
data: unknown,
) {
if (toolName === "get_user_activity" && data && typeof data === "object") {
const value = data as { userId: string; activeDays: number; lastActiveAt: string | null };
state.facts.userId = value.userId;
state.facts.activeDays = value.activeDays;
state.facts.lastActiveAt = value.lastActiveAt;
state.evidence.push({
source: toolName,
summary: `User active days in last 7 days: ${value.activeDays}`,
confidence: 0.9,
});
}

if (toolName === "get_order_status" && data && typeof data === "object") {
const value = data as { hasRecentOrder: boolean; lastOrderStatus: string };
state.facts.hasRecentOrder = value.hasRecentOrder;
state.facts.lastOrderStatus = value.lastOrderStatus;
state.evidence.push({
source: toolName,
summary: `Last order status: ${value.lastOrderStatus}`,
confidence: 0.85,
});
}

if (toolName === "get_ticket_status" && data && typeof data === "object") {
const value = data as { hasOpenTicket: boolean; ticketCount: number };
state.facts.hasOpenTicket = value.hasOpenTicket;
state.facts.ticketCount = value.ticketCount;
state.evidence.push({
source: toolName,
summary: `Open ticket count: ${value.ticketCount}`,
confidence: 0.88,
});
}
}

function buildFinalAnswer(state: AgentState): string {
const evidenceText = state.evidence
.map((item) => `- ${item.summary} (${item.source})`)
.join("\n");

return [
`Goal: ${state.userGoal}`,
"Recommendation: Follow up with this user if activity is low and unresolved support signal exists.",
evidenceText || "No evidence collected.",
].join("\n\n");
}

async function runToolUsingAgent(userGoal: string) {
let state = createInitialState(userGoal);

while (state.status === "running" && state.currentStep < state.maxSteps) {
const decision = await decideNextAction(state);

logStep("tool_agent_decision", decision);

if (decision.type === "finish") {
state = {
...state,
status: "completed",
finalAnswer: decision.answer,
};
break;
}

const tool = getToolByName(decision.toolName);
if (!tool) {
state.status = "failed";
state.lastError = `Tool not found: ${decision.toolName}`;
break;
}

const start = Date.now();
const result = await tool.run(decision.args);
const durationMs = Date.now() - start;

state.toolHistory.push({
step: state.currentStep + 1,
toolName: decision.toolName,
args: decision.args,
resultSummary: result.ok ? JSON.stringify(result.data).slice(0, 160) : String(result.error),
success: result.ok,
durationMs,
});

if (!result.ok) {
state.pendingQuestions.push(`Tool failed: ${decision.toolName}`);
state.lastError = result.error;
} else {
updateFactsFromToolResult(state, decision.toolName, result.data);
}

state.currentStep += 1;
}

if (!state.finalAnswer) {
state.finalAnswer = buildFinalAnswer(state);
}

return state;
}

async function main() {
const state = await runToolUsingAgent(
"Decide whether this user needs priority follow-up",
);

console.log("\n=== FINAL ANSWER ===");
console.log(state.finalAnswer);
}

main().catch((error) => {
console.error("Tool-Using agent failed", error);
process.exit(1);
});

3. 这份模板里优先替换的几个位置

3.1 tools

这是 Tool-Using Agent 第一版最重要的区别之一。

你要先明确:

  • 系统会调用哪几个工具
  • 每个工具只负责什么
  • 返回结构应该长什么样

第一版最好控制在 2 到 3 个工具。

3.2 facts

这个字段代表:

Agent 不只是记录原始结果,而是在积累结构化事实。

后面可以根据业务换成:

  • 用户状态
  • 订单状态
  • 工单状态
  • 内容审核状态

3.3 decideNextAction

这一层后面的常见升级方向是:

  • 把硬编码决策改成模型输出结构化决策
  • 让模型判断先查哪个工具
  • 让模型决定是否继续查别的工具

3.4 toolHistory

这是第一版非常值得保留的东西。

因为它会直接决定后面能不能解释:

  • 为什么这次选了这个工具
  • 为什么最后停在这里
  • 哪一步最容易失败

4. 这份模板怎么扩成真实项目

准备把它从单文件模板扩成真实项目时,可以按这个顺序往下扩:

  1. 先把工具接到真实数据源
  2. 再把 decideNextAction 改成模型决策
  3. 再补最小 trace 和 eval
  4. 最后再考虑缓存、权限、超时和审批

可以直接记成:

先让系统真的会调工具,再考虑让它调得更聪明。

5. 第一版不要急着加什么

这份模板第一版不建议马上补这些:

  • 多 Agent 协作
  • 长期记忆
  • 复杂权限流
  • 太多工具的动态发现
  • 很重的抽象基类体系

因为 Tool-Using Agent 第一版最重要的不是:

  • 工具越多越好
  • 架构越花越好

而是:

  • 工具职责清楚
  • 状态结构清楚
  • 决策和执行边界清楚
  • 最终输出真的基于工具结果

6. 一句话用法

如果只想尽快开始:

  1. 先复制整份模板
  2. 替换 tools
  3. 替换 facts
  4. 先跑通 “选工具 -> 调工具 -> 更新状态 -> 输出建议”

做到这一步,就已经从最小 Agent 走到了真正有多工具决策味道的 Tool-Using Agent 第一版。