惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Y
Y Combinator Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
IT之家
IT之家
量子位
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
罗磊的独立博客
S
SegmentFault 最新的问题
博客园_首页
N
Netflix TechBlog - Medium
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
T
Tailwind CSS Blog
M
MIT News - Artificial intelligence
A
About on SuperTechFans
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
MyScale Blog
MyScale Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Chat Interface Over Any API with TypeScript
Pavel Espiti · 2026-04-27 · via DEV Community

Most AI chat interfaces do the same thing: the user types a message, the LLM generates text, and it appears on screen. But the interesting pattern is when the LLM does something — calls an API, queries a database, runs a command — and then explains what happened.

I built this pattern into AbiLens, a tool that lets you chat with any EVM smart contract. But the architecture generalizes to any external API. You can use the same approach to build a chat interface over a REST API, a database, a CLI tool, or any service with a programmatic interface.

Here's how it works.

The Architecture

The flow has four steps:

  1. User sends a message
  2. LLM decides what API call to make (or responds directly if no call is needed)
  3. Your code executes the API call
  4. LLM explains the result to the user

The key insight: the LLM never calls the API directly. It outputs a structured JSON object describing the call it wants to make. Your code validates and executes it. This keeps the LLM in a sandbox — it can only do what you allow.

User Message
    ↓
System Prompt (with available functions)
    ↓
LLM Response (JSON function call OR plain text)
    ↓
Your Code (validates, executes the call)
    ↓
API Result
    ↓
LLM Explanation (human-readable summary)

Enter fullscreen mode Exit fullscreen mode

The System Prompt Template

The system prompt is where you define what the LLM can do. You list every available function with its name, description, and parameters.

function buildSystemPrompt(
  functions: FunctionDefinition[]
): string {
  const functionList = functions
    .map((fn) => {
      const params = fn.parameters
        .map((p) => `  - ${p.name}: ${p.type}${p.description}`)
        .join("\n");
      return `### ${fn.name}\n${fn.description}\n${params}`;
    })
    .join("\n\n");

  return `You are an assistant that helps users interact with an API.

When the user asks a question that requires data, respond with a JSON function call:
\`\`\`json
{ "function": "functionName", "args": { "key": "value" } }
\`\`\`

When you can answer directly without data, respond in plain text.

Available functions:
${functionList}

Always explain the results in plain language after receiving them.`;
}

Enter fullscreen mode Exit fullscreen mode

For AbiLens, the functions are dynamically generated from the contract's ABI. For a REST API, you'd define them from your OpenAPI spec. For a database, they'd map to common queries.

interface FunctionDefinition {
  name: string;
  description: string;
  parameters: ParameterDefinition[];
}

interface ParameterDefinition {
  name: string;
  type: string;
  description: string;
  required: boolean;
}

Enter fullscreen mode Exit fullscreen mode

Extracting Function Calls from the LLM Response

The LLM's response is either plain text or contains a JSON function call. You need to detect which one it is and extract the structured data.

import { z } from "zod";

const FunctionCallSchema = z.object({
  function: z.string(),
  args: z.record(z.unknown()),
});

type FunctionCall = z.infer<typeof FunctionCallSchema>;

function extractFunctionCall(
  response: string
): FunctionCall | null {
  const jsonMatch = response.match(
    /```
{% endraw %}
json\s*([\s\S]*?)
{% raw %}
```/
  );
  if (!jsonMatch?.[1]) return null;

  const parsed = FunctionCallSchema.safeParse(
    JSON.parse(jsonMatch[1].trim())
  );
  if (!parsed.success) return null;

  return parsed.data;
}

Enter fullscreen mode Exit fullscreen mode

Zod validation here is not optional. LLMs produce malformed JSON, hallucinate function names, and invent parameters. Parse and validate before you execute anything.

Executing the Calls

Map function names to actual implementations. Each handler receives validated arguments and returns a result.

type FunctionHandler = (
  args: Record<string, unknown>
) => Promise<unknown>;

class FunctionRouter {
  private handlers = new Map<string, FunctionHandler>();

  register(
    name: string,
    handler: FunctionHandler
  ): void {
    this.handlers.set(name, handler);
  }

  async execute(call: FunctionCall): Promise<unknown> {
    const handler = this.handlers.get(call.function);
    if (!handler) {
      throw new Error(
        `Unknown function: ${call.function}`
      );
    }
    return handler(call.args);
  }
}

Enter fullscreen mode Exit fullscreen mode

For a REST API wrapper, registration looks like this:

const router = new FunctionRouter();

router.register("getUser", async (args) => {
  const id = z.string().parse(args.id);
  const response = await fetch(`/api/users/${id}`);
  return response.json();
});

router.register("listOrders", async (args) => {
  const status = z.string().optional().parse(args.status);
  const url = new URL("/api/orders", baseUrl);
  if (status) url.searchParams.set("status", status);
  const response = await fetch(url);
  return response.json();
});

Enter fullscreen mode Exit fullscreen mode

Feeding Results Back

After executing the function, send the result back to the LLM for explanation. The conversation history now includes the user's question, the LLM's function call, and the raw result.

async function handleMessage(
  userMessage: string,
  history: Message[],
  router: FunctionRouter,
  llm: LLMClient
): Promise<string> {
  history.push({ role: "user", content: userMessage });

  const response = await llm.chat(history);
  const functionCall = extractFunctionCall(response);

  if (!functionCall) {
    history.push({ role: "assistant", content: response });
    return response;
  }

  const result = await router.execute(functionCall);
  const resultText = JSON.stringify(result, null, 2);

  history.push({ role: "assistant", content: response });
  history.push({
    role: "user",
    content: `Function result:\n${resultText}\n\nExplain this result to the user.`,
  });

  const explanation = await llm.chat(history);
  history.push({
    role: "assistant",
    content: explanation,
  });
  return explanation;
}

Enter fullscreen mode Exit fullscreen mode

The second LLM call is where the value lives. Raw API responses are JSON blobs. The LLM transforms them into answers: "The user has 3 pending orders totaling $142.50, the most recent one placed yesterday."

Error Handling

Things go wrong. The API returns 500. The LLM hallucinates a function that doesn't exist. The arguments are the wrong type. Handle all of these gracefully by feeding the error back to the LLM.

try {
  const result = await router.execute(functionCall);
  // ... feed result back
} catch (error) {
  const errorMessage =
    error instanceof Error
      ? error.message
      : "Unknown error";

  history.push({
    role: "user",
    content: `The function call failed: ${errorMessage}. Let the user know and suggest alternatives.`,
  });

  return llm.chat(history);
}

Enter fullscreen mode Exit fullscreen mode

This creates a self-correcting loop. The LLM sees the error, explains what went wrong, and often suggests a different approach.

Where This Pattern Works

This same architecture applies beyond smart contracts:

  • Database explorer: Define functions for common queries (getTableSchema, runQuery, listTables). The LLM translates natural language into SQL and explains the results.
  • DevOps assistant: Functions for getDeployStatus, listPods, getLogsTail. Chat with your infrastructure.
  • API documentation: Point it at any REST API and let users explore endpoints conversationally.
  • CLI wrapper: Functions map to CLI commands. The LLM picks the right flags and explains the output.

The pattern always looks the same: define available functions, let the LLM choose which to call, execute in a sandbox, explain the results.

Practical Tips

Keep the function list short. More than 15-20 functions degrades LLM accuracy. Group related operations or use a two-step approach where the LLM first picks a category, then a specific function.

Include examples in function descriptions. "Returns the user's order history. Example: { 'userId': '123', 'limit': 10 }" helps the LLM format arguments correctly.

Log every function call. You want a complete audit trail of what the LLM asked for, what you executed, and what came back. This is essential for debugging and for trust.

Rate limit aggressively. The LLM doesn't know about your API quotas. Add rate limiting in your router, not in the LLM prompt.

The full AbiLens source is on my GitHub if you want to see this pattern applied to smart contract interaction. The core chat loop is under 200 lines — most of the complexity lives in the function definitions, not the orchestration.