跪拜 Guibai
← Back to the summary

Building a Local-File Code Agent with LangChain JS and DeepSeek

Foreword

Many AI coding tools (Cursor, CodeGeeX, etc.) now have the ability to read local files and automatically analyze code. The underlying core is LLM Function Calling (Tool Calling). This article uses LangChain JS + the DeepSeek large model to hand-write a lightweight Agent that can read local files and automatically parse code. It does not use high-level Agent executors but manually walks through the complete chain of "model determines to call a tool → executes local I/O → tool result is sent back to the model → outputs code interpretation" to thoroughly understand the underlying principles of Tool calling.

Tech Stack Description

  1. @langchain/openai: Unified LLM encapsulation, compatible with all large models following the OpenAI protocol.
  2. @langchain/core: Provides the tool decorator and four standard message base classes.
  3. zod: Strong type validation for tool parameters, standardizing the format of parameters passed by the LLM.
  4. dotenv: Manages API keys to prevent hardcoding leaks.
  5. node:fs/promises: Node.js native asynchronous file read/write capability.

I. Prerequisite Environment and Dependency Installation

1.1 Project Initialization

bash

mkdir hello-langchain && cd hello-langchain
npm init -y

1.2 Install All Dependencies

bash

npm install dotenv @langchain/openai @langchain/core zod

1.3 Configure Environment Variables

Create a .env file in the project root directory and fill in your DeepSeek key:

env

DEEPSEEK_API_KEY=sk-xxxYourKeyxxx

II. Complete Runnable Source Code (Line-by-Line Commented Version)

File name: tool.mjs

javascript

// Load .env environment variables into process.env. Repeated imports have no effect; it executes only once.
import 'dotenv/config';

// Import the LangChain-wrapped OpenAI-compatible model class.
// Note: Although the name includes OpenAI, all models supporting the OpenAI interface format can be used (DeepSeek, Tongyi Qianwen, etc.)
import { ChatOpenAI } from '@langchain/openai';

// Import the tool definition function, used to wrap ordinary JS functions into standard tools recognizable by the LLM.
import { tool } from '@langchain/core/tools';

// Import LangChain's four standard message objects to maintain the complete conversation context.
import {
    HumanMessage,  // Message sent by the user, corresponding to role: 'user'
    SystemMessage, // System prompt message, corresponding to role: 'system', sets the AI's role and rules
    ToolMessage,   // Tool execution result message, corresponding to role: 'tool', passes the tool's return value to the model
    AIMessage      // Message returned by the AI model, corresponding to role: 'assistant'
} from '@langchain/core/messages';

// Import Node.js native asynchronous file module, returns a Promise, can be used with await.
import fs from 'node:fs/promises';

// Import the Zod type validation library to constrain the format of tool input parameters.
import { z } from 'zod';

// ===================== 1. Initialize the Large Model Instance =====================
const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',       // Specifies the model name to use
    apiKey: process.env.DEEPSEEK_API_KEY, // Reads the key from environment variables; hardcoding is prohibited
    temperature: 0,                       // Temperature value 0 = completely deterministic output, suitable for code and tool-calling scenarios
    configuration: {
        baseURL: 'https://api.deepseek.com/v1' // DeepSeek's OpenAI-compatible interface address
    }
});

// ===================== 2. Define the File Reading Tool =====================
// tool() receives two parameters: ① the tool execution function ② tool metadata (the instruction manual for the model)
const readFileTool = tool(
    // First parameter: The asynchronous logic that the tool actually executes
    async ({ filePath }) => {
        // Read the file at the specified path in UTF-8 format
        const content = await fs.readFile(filePath, 'utf-8');
        // Print a log to the console for easy debugging, to observe when the model calls the tool
        console.log(`[Tool Call]read_file(${filePath}) successfully read ${content.length} bytes of content`);
        // The return value will be wrapped into a ToolMessage and eventually passed back to the large model
        return content;
    },
    // Second parameter: Tool metadata. The large model relies on this information to determine "when to call and what parameters to pass"
    {
        name: 'read_file', // Unique tool name. The model matches the tool by name when calling it.
        description: `Reads the content of a local file. This tool must be called when the user requests to view code, analyze a file, or read a script; supports relative/absolute paths.`,
        schema: z.string().describe('The file path to be read') // Parameter constraint: must be a string, with a description
    }
);

// Tool list array. To add new tools later, simply append them to this array.
const tools = [readFileTool];

// ===================== 3. Bind Tools to the Model =====================
// bindTools injects the tool descriptions into the model request, letting the model know what tools are available.
const modelWithTools = model.bindTools(tools);

// ===================== 4. Initialize the Conversation Context Message Queue =====================
// The messages array saves the complete conversation history. Messages are appended to this array in each round of interaction.
const messages = [
    // System prompt: Sets the AI's role, workflow, and behavioral constraints
    new SystemMessage(`
You are a professional code assistant. You can only obtain local file content through the read_file tool. Strictly follow the process:
1. When the user requests to read/parse a file, you must call the read_file tool; fabricating code is prohibited.
2. Wait for the tool to return the complete file text.
3. Based on the real file content, perform logical breakdown, line-by-line commenting, and problem analysis.
Available tool: read_file reads local files
    `),
    // User's question: The entry point that triggers the tool call
    new HumanMessage('Please read the tool.mjs file content and fully explain this LangChain tool-calling code')
];

// ===================== 5. Main Execution Flow (Complete Tool Calling Chain) =====================
async function runAgent() {
    // First round invoke: Send the conversation history to the model, letting the model reason whether a tool call is needed.
    // invoke = Initiates a complete model inference request and waits for all results to return.
    let response = await modelWithTools.invoke(messages);
    // Add the model's returned result to the conversation history to maintain context.
    messages.push(response);

    // Determine if the model returned a tool call instruction.
    if (response.tool_calls?.length > 0) {
        // Iterate through all tool calls (supports calling multiple tools simultaneously).
        for (const toolCall of response.tool_calls) {
            const { name, args, id } = toolCall;
            let toolResult;

            // Match by tool name and execute the corresponding tool logic.
            if (name === 'read_file') {
                // Call the tool's invoke method, pass in the arguments, and get the execution result.
                toolResult = await readFileTool.invoke(args);
            }

            // Wrap the tool execution result into a ToolMessage. Must carry the tool_call_id.
            // The model uses this id to match: which tool call does this result correspond to?
            messages.push(new ToolMessage({
                tool_call_id: id,
                content: toolResult
            }));
        }

        // Second round invoke: Send the tool results together to the model, so the model can generate the final answer based on the real file content.
        const finalAns = await modelWithTools.invoke(messages);
        console.log("\n===== AI Code Analysis Result =====");
        console.log(finalAns.content);
    } else {
        // If no tool call is needed, directly print the model's text response.
        console.log(response.content);
    }
}

// Start the Agent
runAgent();

III. In-Depth Module-by-Module Breakdown of Core Code

3.1 Dependency Import Module: The Role of Each Package

Table

Import Item Core Role Common Pitfalls
dotenv/config Automatically reads the root .env file and injects variables into process.env Repeated imports are ineffective. Writing it twice in the code is redundant; not an error, but not recommended.
ChatOpenAI Unified LLM call entry point, compatible with all OpenAI protocol models When connecting to non-OpenAI models, baseURL must be changed, otherwise it will request the OpenAI official site.
tool Standardized tool wrapping function, automatically generates tool descriptions that meet model requirements The input parameter structure is fixed: the execution function first, then the metadata object.
Four Message Types Unified conversation message format, maintains the context chain ToolMessage must carry tool_call_id, otherwise the model cannot match the context.
z (Zod) Runtime parameter validation, forces the LLM to pass parameters in the correct format The clearer the schema description, the higher the accuracy of the model's parameter passing.

3.2 Large Model Initialization: Why It Can Connect to DeepSeek This Way

DeepSeek officially provides an OpenAI-compatible interface, so you can directly use the ChatOpenAI class to connect by modifying only two configurations:

  1. Change baseURL to DeepSeek's interface address.
  2. Change modelName to DeepSeek's model name.

Key Configuration temperature: 0

3.3 Tool Definition: The Two-Part Structure is the Core

tool(execution function, metadata) is the standard way to define tools in LangChain. The two parts have distinct responsibilities:

Part One: Asynchronous Execution Function

Part Two: Metadata Configuration

3.4 bindTools: Equipping the Model with a "Toolbox"

js

const modelWithTools = model.bindTools(tools);

bindTools does two things:

  1. Formats all tool names, descriptions, and parameter formats according to the OpenAI tool-calling protocol.
  2. Automatically stuffs the tool list into the request body sent to the model each time it is called.

The bound modelWithTools has two possible outputs:

3.5 Conversation Context: Why a Message Array is Needed

LLMs are stateless. The complete conversation history must be sent with every request for the model to know what was said before and what the tool returned.

The collaboration flow of the four message types:

  1. SystemMessage sets the rules first.
  2. HumanMessage is the user's request.
  3. AIMessage is the model's reply "I want to call a tool."
  4. ToolMessage returns the tool's execution result.
  5. The model then outputs the final answer.

3.6 Main Flow: What the Two invokes Do Respectively

The entire flow calls invoke twice. This is the standard chain for tool calling:

First invoke: Determine whether to use a tool

Second invoke: Generate an answer after getting the result

invoke is LangChain's unified execution entry point. Whether it's a model, a tool, or a chain, execution is done using .invoke(input), meaning "pass in input, execute logic, return the complete result."

IV. Key Knowledge Point: Analysis of the tool_calls Data Structure

A simplified example of the core structure of the AIMessage returned by the model:

json

{
  "content": "",
  "tool_calls": [
    {
      "name": "read_file",
      "args": { "input": "tool.mjs" },
      "id": "call_00_TjGbBoBzcvEKkL56qzbF9059"
    }
  ],
  "finish_reason": "tool_calls"
}

V. Common Pitfalls and Solutions

5.1 ERR_MODULE_NOT_FOUND Cannot Find dotenv

5.2 Model Still Fabricates Code After Tool Call

5.3 Timer 'xxx' does not exist

The label names for console.time and console.timeEnd must be exactly identical, including case, otherwise a timer does not exist error will be thrown.

5.4 Promise Serial vs. Parallel Execution Time

VI. Extension: Multi-Tool Parallelism and Promise.all

Complex Agents may call multiple tools simultaneously. Serial execution can be very slow. Using Promise.all for concurrent execution can significantly speed things up:

js

// Wrap all tool calls into an array of Promises
const toolTasks = response.tool_calls.map(async (call) => {
  let result;
  if (call.name === 'read_file') result = await readFileTool.invoke(call.args);
  // Expand matching for new tools here later
  return new ToolMessage({
    tool_call_id: call.id,
    content: result
  });
});

// Wait for all tool executions to complete in parallel
const toolMessages = await Promise.all(toolTasks);
// Add all to the context at once
messages.push(...toolMessages);

VII. Summary

  1. Core logic of Tool calling: LLM identifies the scenario and issues an instruction → Local execution of I/O / API → Result is passed back to the model to generate the final answer.
  2. tool() two-part structure: Business execution function + Zod parameter constraint + description metadata; none are dispensable.
  3. Four message types maintain the complete context. tool_call_id is the key to associating the tool with the model's context.
  4. ChatOpenAI is compatible with all OpenAI protocol large models. Switching models only requires modifying baseURL and modelName.
  5. Manually implementing the tool chain allows a thorough understanding of the underlying operating principles of code AIs like Cursor and CodeGeeX. It can later be easily extended with more Agent capabilities such as file writing, command execution, and knowledge base retrieval.