返回本课配套文件

main.ts

code/main.ts下载本课全部文件
// 模块4《Agent工程》 Lesson 01 — toy ReAct agent loop, in TypeScript.
//
// Mirrors code/main.py: message buffer, tool registry, stop condition,
// turn budget, observation formatter. The model is a scripted ToyLLM so the
// loop runs offline and deterministic; swap for a real provider client and
// the control flow is identical.
//
// References:
//   ReAct paper       https://arxiv.org/abs/2210.03629
//   Anthropic agents  https://www.anthropic.com/engineering/building-effective-agents
//
// Run: npx tsx code/main.ts

type ToolFn = (args: Record<string, string>) => string;

type ToolCall = {
  name: string;
  args: Record<string, string>;
};

type Turn = {
  kind: "user" | "thought" | "action" | "final";
  content: string;
  toolCall?: ToolCall;
  observation?: string;
};

class ToolRegistry {
  private tools = new Map<string, ToolFn>();

  register(name: string, fn: ToolFn): void {
    this.tools.set(name, fn);
  }

  names(): string[] {
    return [...this.tools.keys()].sort();
  }

  dispatch(call: ToolCall): string {
    const fn = this.tools.get(call.name);
    if (!fn) return `error: unknown tool ${JSON.stringify(call.name)}`;
    try {
      return fn(call.args);
    } catch (err) {
      const e = err as Error;
      return `error: ${e.name}: ${e.message}`;
    }
  }
}

function calculator(args: Record<string, string>): string {
  const expr = args.expr;
  if (typeof expr !== "string") return "error: missing expr";
  if (!/^[0-9+\-*/(). ]+$/.test(expr)) {
    return "error: illegal character in expr";
  }
  try {
    const fn = new Function(`"use strict"; return (${expr});`);
    const value = fn();
    if (typeof value !== "number" || !Number.isFinite(value)) {
      return `error: non-finite result for ${expr}`;
    }
    return String(value);
  } catch (err) {
    const e = err as Error;
    return `error: ${e.name}: ${e.message}`;
  }
}

class KVStore {
  private store = new Map<string, string>();

  get = (args: Record<string, string>): string => {
    const key = args.key;
    if (!this.store.has(key)) return `missing:${key}`;
    return this.store.get(key) as string;
  };

  set = (args: Record<string, string>): string => {
    this.store.set(args.key, args.value);
    return `stored ${args.key}`;
  };
}

type ScriptEntry =
  | { kind: "action"; thought: string; action: string; args: Record<string, string> }
  | { kind: "finish"; content: string };

// Scripted ReAct policy. Returns one assistant turn per call.
// Replace with a provider client and the loop is identical.
class ToyLLM {
  private cursor = 0;
  constructor(private script: ScriptEntry[]) {}

  respond(_history: Turn[]): ScriptEntry {
    if (this.cursor >= this.script.length) {
      return { kind: "finish", content: "no more actions" };
    }
    return this.script[this.cursor++];
  }
}

class AgentLoop {
  history: Turn[] = [];

  constructor(
    private llm: ToyLLM,
    private tools: ToolRegistry,
    private maxTurns = 12,
  ) {}

  run(userMessage: string): string {
    this.history.push({ kind: "user", content: userMessage });
    for (let step = 0; step < this.maxTurns; step++) {
      const reply = this.llm.respond(this.history);
      if (reply.kind === "finish") {
        this.history.push({ kind: "final", content: reply.content });
        return reply.content;
      }
      this.history.push({ kind: "thought", content: reply.thought });
      const call: ToolCall = { name: reply.action, args: reply.args };
      const observation = this.tools.dispatch(call);
      this.history.push({
        kind: "action",
        content: call.name,
        toolCall: call,
        observation,
      });
    }
    this.history.push({ kind: "final", content: "budget exhausted" });
    return "budget exhausted";
  }

  toolNames(): string[] {
    return this.tools.names();
  }
}

function prettyTrace(history: Turn[]): void {
  history.forEach((turn, i) => {
    const tag = `[${String(i).padStart(2, "0")} ${turn.kind.padStart(7)}]`;
    if (turn.kind === "user" || turn.kind === "thought" || turn.kind === "final") {
      console.log(`${tag} ${turn.content}`);
    } else if (turn.kind === "action" && turn.toolCall) {
      const argText = JSON.stringify(turn.toolCall.args);
      console.log(`${tag} ${turn.toolCall.name}(${argText}) -> ${turn.observation}`);
    }
  });
}

function buildDemoAgent(): AgentLoop {
  const tools = new ToolRegistry();
  tools.register("calculator", calculator);
  const kv = new KVStore();
  tools.register("kv_get", kv.get);
  tools.register("kv_set", kv.set);

  const script: ScriptEntry[] = [
    {
      kind: "action",
      thought: "store the base price",
      action: "kv_set",
      args: { key: "base", value: "120" },
    },
    {
      kind: "action",
      thought: "compute 15% tax",
      action: "calculator",
      args: { expr: "120 * 0.15" },
    },
    {
      kind: "action",
      thought: "store the tax",
      action: "kv_set",
      args: { key: "tax", value: "18.0" },
    },
    {
      kind: "action",
      thought: "compute total",
      action: "calculator",
      args: { expr: "120 + 18.0" },
    },
    {
      kind: "action",
      thought: "confirm stored values",
      action: "kv_get",
      args: { key: "base" },
    },
    { kind: "finish", content: "the total including 15% tax is 138.0" },
  ];
  return new AgentLoop(new ToyLLM(script), tools, 10);
}

function main(): void {
  console.log("=".repeat(70));
  console.log("TOY REACT LOOP — 模块4《Agent工程》, Lesson 01 (TypeScript port)");
  console.log("=".repeat(70));

  const agent = buildDemoAgent();
  const final = agent.run("What is 120 plus 15% tax, stored in kv?");
  console.log();
  prettyTrace(agent.history);
  console.log();
  console.log(`final answer: ${final}`);
  const actions = agent.history.filter((t) => t.kind === "action").length;
  console.log(`turns used:   ${actions}`);
  console.log(`tools used:   ${JSON.stringify(agent.toolNames())}`);
}

main();