从0学AI · Day 1: 让大模型真正动手干活

4 阅读7分钟

从0学AI · Day 1: 让大模型真正动手干活:

你有没有想过一个问题:为什么 AI 能写出代码,却不能帮你跑通代码?

我前两天想让 AI 帮我创建一个 React 项目,就是那种带 TodoList 的简单项目。我跟它说了一堆需求,它哗哗哗给我返回了一堆代码,然后呢?

然后就没有然后了。

我得自己去开终端、去敲命令、去建文件夹、去复制粘贴。不是说 AI 能替我干活吗?怎么还得我当打字员?

所以这篇文章要回答的问题是:怎么让大模型真正具备「动手能力」?


LangChain 有一套 Tool Calling 机制,说到底就是让 AI 不只是返回文字,而是能调用工具。

读文件、执行命令、列目录,这些活儿它都能干。

我寻思了一下,不如自己动手撸一个工具,叫 AI 动手实验室。顾名思义,就是让 AI 在这儿真正动手帮你干活。

目标很简单,用户说一句话,AI 自己动手去执行,能创建项目、能写代码、能安装依赖、最后把结果给你亮出来。

有点意思。


先说技术方案。

LangChain 那边给了个 tool() 装饰器,用起来其实挺直白的。你定义一个 async 函数,然后给它裹上一层 metadata——name 是工具名字,description 是工具干啥的,argsSchema 是参数定义,用 Zod 写。如果函数签名足够清晰,argsSchema 也可以省略,LangChain 会自动从参数类型推断出来。

import { tool } from '@langchain/core/tools'

const ailabReadFile = tool(
  async ({ filePath }) => {
    try {
      const content = await fs.readFile(filePath, 'utf-8')
      return content
    } catch (err) {
      return `读取失败:${err.message}`
    }
  },
  {
    name: 'ailab_read_file',
    description: '读取指定路径的文件内容,返回文件完整文本,输入参数是完整文件路径,读取失败时返回错误信息',
    argsSchema: z.object({
      filePath: z.string().describe('完整文件路径'),
    }),
  },
)

就这么简单。

在 AI 动手实验室里,我给自己定义了四个工具,读文件、写文件、执行命令、列目录。你看着可能觉得平平无奇,但重点在于,这个 description 会被塞给模型,模型会根据这个 description 决定要不要调用这个工具。

说到底,description 就是你跟模型沟通的语言。描述清楚了,模型才知道什么时候该动手。


然后说 Agent 的执行循环。

这块是整个方案的核心,代码比文字好懂:

async function ailabRunAgent(query, maxIterations = 30) {
  const systemPrompt = '你是一个 AI 助手,可以使用工具完成任务。'
  const messages = [new SystemMessage(systemPrompt), new HumanMessage(query)]
  const toolRegistry = new Map()

  for (const t of ailabTools) {
    toolRegistry.set(t.name, t)
  }

  for (let i = 0; i < maxIterations; i++) {
    const response = await ailabModel.invoke(messages)
    messages.push(response)

    if (!response.tool_calls?.length) {
      return response.content
    }

    for (const toolCall of response.tool_calls) {
      const matchedTool = toolRegistry.get(toolCall.name)
      if (!matchedTool) {
        messages.push(new ToolMessage({
          content: `错误:未找到工具 ${toolCall.name}`,
          tool_call_id: toolCall.id,
        }))
        continue
      }
      const toolResult = await matchedTool.invoke(toolCall.args)
      messages.push(new ToolMessage({
        content: toolResult,
        tool_call_id: toolCall.id,
      }))
    }

    if (i === maxIterations - 1) {
      return '任务执行已达最大步数上限,请简化任务后重试'
    }
  }
}

逻辑很朴素。

第一,调用模型,看看它想干嘛。

第二,检查返回结果里有没有 tool_calls。如果没有,说明模型决定自己回答,不需要调用工具,那就直接把回答返回去。

第三,如果有工具调用,那就一个个执行,把结果塞回消息列表,然后继续下一轮循环。

第四,加个 maxIterations 保护,免得模型死循环跟你耗到天亮。

这里涉及一个取舍:设太小,复杂任务跑不完;设太大,又怕模型跑偏。这个平衡挺微妙的,我自己也还在摸索。

这种模式有个名字,叫 ReAct,就是 Reasoning 加 Acting。先推理,再行动,然后继续推理、继续行动,直到模型觉得行了。


有个设计细节值得琢磨一下。

execute_command 工具(叫 ailab_exec)支持一个 workingDirectory 参数,就是可以指定命令在哪个目录下跑,而不需要在命令里头写 cd。

一开始我还觉得这多此一举,后来踩了坑才明白这设计的精妙。

你想想,如果用户在命令里写 cd project && pnpm install,这个命令字符串会先经过 shell 解析。然后如果 workingDirectory 已经设成了 project,你再 cd project,系统会找不到目录。

所以正确的用法是这样的:

command: "pnpm install",
workingDirectory: "react-todo-app"

workingDirectory 负责切换目录,command 里头不需要也不能出现 cd。

说到底就是职责分离。工具内部处理目录切换,调用者不需要操心 cd 的事儿。


说到工具定义,有个点我觉得挺重要的,就是 description 这个字段。

它不只是给人类看的文档,更重要的是它是给模型看的指令。模型根据这个 description 决定要不要调用这个工具、什么时候调用、用什么参数调用。

一个好的 description 应该包含三样东西:

  • 功能说明:工具能干啥
  • 参数含义:每个参数是啥、干啥用、返回啥
  • 使用场景:啥情况该用它、啥情况不该用它

反面例子是这样的,「读取文件」四个字,模型看了也懵,不知道该在啥情况下调用。

正面例子是这样的:「读取指定路径的文件内容,返回文件完整文本,输入参数是完整文件路径,失败时返回错误信息。」

你看,这样描述清楚了,模型才知道啥时候该找你。


然后我想聊聊这种 Tool Calling 能力的想象空间。

现在在 AI 动手实验室里,我给它定义的是读文件、写文件、执行命令、列目录这些基础操作。但这里有一个问题值得思考:如果把工具扩展一下呢?

可以有个搜索工具,去 Google、去 DuckDuckGo 搜信息。可以有个数据库工具,直接查 SQL 数据。可以有个 API 调用工具,去请求外部服务。可以有个代码执行工具,在沙盒环境里跑 Python 代码。

再往大了想。

如果给它接上浏览器自动化工具,它是不是可以自己去网页上抓数据?如果接上 Git 工具,它是不是可以自己管理版本?如果接上 Docker 工具,它是不是可以自己构建和部署服务?

这些工具组合起来,就是真正可以自主决策、自主执行、自主修正的 AI Agent 了。Claude Code、Codex 这些产品走的就是这个方向,但落地还需要不少工程打磨。

有意思的是,想到这一步的时候,我发现这个方向的本质,其实是把 AI 从一个"说话的工具"变成一个"做事的工具"。问题关键在于,怎么定义好工具的边界,怎么设计好工具之间的协作。


当然,路还没那么好走。

第一个问题是命令注入。就是如果用户故意输入恶意指令,比如让 AI 去删系统文件,那咋整?

现有的防护手段——比如在 SystemMessage 里写规则约束模型行为——在没有恶意用户的情况下是够用的,但面对主动攻击时,规则约束的脆弱性就会暴露出来。这里有个矛盾点:如果把规则写得太死,模型的灵活性就没了;如果写得太松,安全风险就大了。

生产环境下,你需要配合白名单机制、权限控制、命令审计这些多层防线,不能只靠模型自觉。

第二个问题是提示词注入。这是另一个维度的问题——用户输入的内容可能会夹带恶意指令,污染 SystemMessage 中的约束规则。这个和命令注入来源不同,但同样需要认真对待。

安全这一块儿,水挺深的,今天先不展开。

还有一个问题是长任务控制。如果一个任务特别复杂,需要几十轮工具调用,那怎么控制节奏?怎么防止模型在某个分支上死磕?

我现在的方案是设一个 maxIterations 上限。但这里涉及一个取舍:设多少算够?设太小,复杂任务跑不完;设太大,又怕模型跑偏。这个平衡挺微妙的,我自己也还在摸索。


好了,总结一下今天聊的。

第一,LangChain 提供了 tool() 装饰器,可以很方便地定义工具。工具函数是 async 的,metadata 里头 name、description 是必须的,argsSchema 通常需要,如果函数签名够清晰也可以省略自动推断。

第二,模型通过 bindTools() 绑定工具,然后就能在推理过程中决定是否调用工具、调用哪些工具。

第三,Agent 的执行循环就是不断调用模型、执行工具、把结果塞回消息列表,直到模型认为不需要再调用工具了。

第四,Tool Calling 的想象空间很大,从文件操作到浏览器自动化到数据库查询到代码执行,组合起来就是真正的 AI Agent。Claude Code、Codex 等产品走的就是这个方向。

第五,生产环境还有命令注入、提示词注入、权限控制、长任务控制这些问题需要解决。说到底,这些问题的本质是同一个:怎么让 AI 在"能做事"和"不出事"之间找到平衡。


你要是想自己跑一遍,代码我放在 GitHub 了。去仓库看吧。

以前我们跟 AI 聊天,它给我们文字。现在我们让 AI 干活,它给我们结果。

有意思的方向。


好,以上就是今天学习 AI 的第一天的全部内容。

我是真心觉得这篇文章对你有帮助,随手点个赞、在看、转发三连吧。

我是 Spark.Z,一个个人开发者,我们下次再见。

/ 作者:Spark.Z / 交流讨论,请联系邮箱:zhangjinlinqq@gmail.com