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. 这份模板怎么扩成真实项目
准备把它从单文件模板扩成真实项目时,可以按这个顺序往下扩:
- 先把工具接到真实数据源
- 再把
decideNextAction改成模型决策 - 再补最小 trace 和 eval
- 最后再考虑缓存、权限、超时和审批
可以直接记成:
先让系统真的会调工具,再考虑让它调得更聪明。
5. 第一版不要急着加什么
这份模板第一版不建议马上补这些:
- 多 Agent 协作
- 长期记忆
- 复杂权限流
- 太多工具的动态发现
- 很重的抽象基类体系
因为 Tool-Using Agent 第一版最重要的不是:
- 工具越多越好
- 架构越花越好
而是:
- 工具职责清楚
- 状态结构清楚
- 决策和执行边界清楚
- 最终输出真的基于工具结果
6. 一句话用法
如果只想尽快开始:
- 先复制整份模板
- 替换
tools - 替换
facts - 先跑通 “选工具 -> 调工具 -> 更新状态 -> 输出建议”
做到这一步,就已经从最小 Agent 走到了真正有多工具决策味道的 Tool-Using Agent 第一版。