理解ChatBot 和 Agent的区别

之前我们实现了和大模型进行有记忆的对话功能!

iShot_2026-05-16_12.10.06.gif

但是你想让他帮你能够做一些实际有用的事情,他可能还没办法能完全做到,比如你让他去查询天气这种事情

➜  agent-sdy git:(main) ✗ npm run start

> agent-sdy@1.0.0 start
> tsx src/index.ts


 User Say:今天广州的天气怎么样
我无法实时获取当前天气信息。不过,你可以通过以下方式快速查看今天广州的天气:

✅ 推荐方式:  
- 打开手机自带天气App(如苹果天气、华为天气、小米天气)  
- 在微信中搜索“广州天气”,使用“中国天气网”或“广东天气”官方小程序  
- 访问权威网站:[中国气象局官网](http://www.cma.gov.cn) 或 [广东天气网](https://gd.tianqi.com/)  

>  小贴士:  
> )多为“龙舟水”季节,常有阵雨或雷阵雨,湿度大、气温25–32℃左右,偶有短时强降水或雷电。出门建议带伞,注意防潮防雷。

需要我帮你解读天气预报术语(如“阵雨转多云”“相对湿度90%”),或提供穿衣/出行建议,欢迎随时告诉我! 😊

他只能教你怎么做,并不能真的帮你动手去查,他没有工具可以帮你查出来真实的信息,然后告诉你!

这就是 ChatBot 和 Agent 的本质区别—ChatBot 只能说,Agent 能做。

之前的ChatBot的工作流程是 你问一句,大模型回答,然后再次执行 Main 方法 进入下一次的用户输入环节,然后大模型回复!

agent 的思想就完全不一样了!可以先理清楚这个设计思路,后面我们去写代码,就会清晰

我们如果想让chatbot实现帮你查询天气的功能,那么就需要提供这个查询的工具给大模型,当你去查询天气的时候

大模型发现:哎呀,有个查询天气的工具,然后就说我要去调用查询天气的工具了,然后查询好天气,再把结果整理好,然后再发送会给用户!

大模型这一套的流程 我们称之为 Think Action Observe 思考,行动 ,观察,

Think 思考:改调用什么工具

Action 行动:通过工具拿到查询结果

Observe 观察:结果是不是满足用户的需求

然后回到 Think,直到模型认为可以给出最终回答

所以这里能想到,我们需要一个循环在里面,让大模型执行这一套流程!

如何定义一个工具

一个工具由三样东西组成:

1.description:告诉模型这个工具是干什么的(模型靠这个判断什么时候该调它)

模型通过这段描述来判断"用户问天气的时候,我应该调这个工具"。描述写得越准确,模型调用的时机就越精准。

这里工具调用的本质:其实大模型并不会帮你执行真的去调用代码的函数去执行,他只是看到了你的参数长什么样子~ 然后去拼装对应的数据结构给到你,然后SDK内部拿这些参数去执行了函数,并不是大模型测真的可以支持直接调用你的函数代码

2.inputSchema:工具接受什么参数(用 JSON Schema 定义)

jsonSchema() 定义工具的参数结构

本质就是一段 JSON Schema。AI SDK 会把它跟 description 一起塞进请求发给模型

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "查询指定城市的天气信息",
    "parameters": {
      "type": "object",
      "properties": {
        "city": { "type": "string", "description": "城市名称" }
      },
      "required": ["city"]
    }
  }
}

工具的 description 和 inputSchema 里的属性 description,本质上就是在写 prompt。

你写得越清楚、越具体,模型调用的准确率就越高。

"查天气"不如"查询指定城市的实时天气信息,包括温度、风向等"。

3.execute:实际执行函数 就是一个普通的 async 函数。模型决定调用工具时,SDK 会自动用模型返回的参数调用 execute,然后把返回值序列化成字符串,作为 tool-result 消息塞回对话历史里。

下面是两个工具示例:

import { jsonSchema } from 'ai';

export const weatherTool = {
  
  description: '查询指定城市的天气信息',
  
  inputSchema: jsonSchema({
    type: 'object',
    properties: {
      city: { type: 'string', description: '城市名称,如"北京"、"上海"' },
    },
    required: ['city'],
    additionalProperties: false,
  }),
  
  execute: async ({ city }: { city: string }) => {
    // 先用假数据,后面课程会接真实 API
    const mockWeather: Record<string, string> = {
      '北京': '晴,15-25°C,东南风 2 级',
      '上海': '多云,18-22°C,西南风 3 级',
      '广州': '小雨,21-32°C,南风 2 级',
      '深圳': '阵雨,22-28°C,南风 2 级',
    };
    return mockWeather[city] || `${city}:暂无数据`;
  },
};


export const calculatorTool = {
  description: '计算数学表达式的结果。当用户提问涉及数学运算时使用',
  
  inputSchema: jsonSchema({
    type: 'object',
    properties: {
      expression: { type: 'string', description: '数学表达式,如 "2 + 3 * 4"' },
    },
    required: ['expression'],
    additionalProperties: false,
  }),
  
  execute: async ({ expression }: { expression: string }) => {
    try {
      // 生产环境不要用 eval,这里纯粹为了演示
      const result = new Function(`return ${expression}`)();
      return `${expression} = ${result}`;
    } catch {
      return `无法计算: ${expression}`;
    }
  },
};

我们可以试试用上面两个工具

const tools = {
  get_weather: weatherTool,
  calc_math: calculatorTool,
};

tools 是一个对象,key 是工具名(模型在调用时会引用这个名字),value 是工具定义对象。

然后调用的时候,可以把tools传递给大模型 此时你再去执行之前的代码,可能发现Cli 终端什么都不会输出了

const result = await streamText({
  model,
  messages,
  tools,
});

我们加个console来调试一下为什么

console.log(JSON.stringify(result)) 
{
    "_totalUsage": {
        "status": {
            "type": "pending"
        }
    }, 
    "_finishReason": {
        "status": {
            "type": "pending"
        }
    }, 
    "_rawFinishReason": {
        "status": {
            "type": "pending"
        }
    }, 
    "_steps": {
        "status": {
            "type": "pending"
        }
    }, 
    "includeRawChunks": false, 
    "tools": {
        "get_weather": {
            "description": "查询指定城市的天气信息", 
            "inputSchema": {
                "jsonSchema": {
                    "type": "object", 
                    "properties": {
                        "city": {
                            "type": "string", 
                            "description": "城市名称,如\"北京\"、\"上海\""
                        }
                    }, 
                    "required": [
                        "city"
                    ], 
                    "additionalProperties": false
                }
            }
        }, 
        "calc_math": {
            "description": "计算数学表达式的结果。当用户提问涉及数学运算时使用", 
            "inputSchema": {
                "jsonSchema": {
                    "type": "object", 
                    "properties": {
                        "expression": {
                            "type": "string", 
                            "description": "数学表达式,如 \"2 + 3 * 4\""
                        }
                    }, 
                    "required": [
                        "expression"
                    ], 
                    "additionalProperties": false
                }
            }
        }
    }, 
    "baseStream": { }
}

当前轮次正在 等待调用 所有字段都是 pending = 初始化完成、工具已注册、流程待命

从目前的返回结果来看,我们并不知道大模型下一步的决策是什么也不好继续做下一步的处理

所以SDK提供了其他的方法 result.fullStream

textStream 只给你文本片段

但现在模型除了文本,还可能返回工具调用——textStream 会把这些全部丢掉。

fullStream 包含完整的事件流,每个事件都有 type 字段告诉你发生了什么:让我们可以知道大模型准备做什么,我们可以根据这个来处理里面的逻辑!

for await (const chunk of result.fullStream) {
  console.log()
  process.stdout.write(chunk.type);
  fullRep += chunk;
}

执行我们来看看,这里的类型

➜  agent-sdy git:(main) ✗ npm run start

> agent-sdy@1.0.0 start
> tsx src/index.ts


 User Say:查询广州的天气
{"_totalUsage":{"status":{"type":"pending"}},"_finishReason":{"status":{"type":"pending"}},"_rawFinishReason":{"status":{"type":"pending"}},"_steps":{"status":{"type":"pending"}},"includeRawChunks":false,"tools":{"get_weather":{"description":"查询指定城市的天气信息","inputSchema":{"jsonSchema":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,如\"北京\"、\"上海\""}},"required":["city"],"additionalProperties":false}}},"calc_math":{"description":"计算数学表达式的结果。当用户提问涉及数学运算时使用","inputSchema":{"jsonSchema":{"type":"object","properties":{"expression":{"type":"string","description":"数学表达式,如 \"2 + 3 * 4\""}},"required":["expression"],"additionalProperties":false}}}},"baseStream":{}}

start 	#会话开始
step-start	#开始单步推理
text-start	#开始输出文本
tool-input-start	#开始生成工具参数
tool-input-delta	#流式生成工具入参片段
tool-input-end	#工具入参生成完毕
tool-call	#正在调用工具
tool-result	#拿到工具返回结果
text-delta	#实时输出文字
text-end	#文本输出结束
step-finish	#单步完成
finish	#全部流程结束

 User Say:

所以我们核心就是循环我们的调用AI的过程!在中间持续观察AI的运行状态,如果满足推出状态才能推出!

while(something..){
  const result = await streamText({
  	model,
  	messages,
  	tools,
	});
  
  // 然后根据结果判断是不是还有继续循环
}

把每轮的调用结果都给到下一次调用,直到 AI 觉得可以输出给用户了,就跳出循环!

上面的所有事件我们只需要关注下面的即可:

  • text-delta:文本片段(跟 textStream 一样)

  • tool-call:模型决定调用某个工具,包含工具名和参数

  • tool-result:工具执行完毕,包含返回值

  • step-start / step-finish:每一步的开始和结束

  • finish:所有步骤都完成了

const maxLoop = 30; // 最大循环次数,防止AI可能幻觉导致死循环

let current = 0;
while (current < maxLoop) {
  
  let hasToolCall = false; // 判断本轮是否有工具调用
  
  // 核心,每次循环都会调用,根据这个结果判断还是否会有下次循环
  const result = await streamText({
    model,
    messages,
    tools,
  });
  
  
  // 处理结果
  for await (const chunk of result.fullStream) {
    switch (chunk.type) {
        
      case "start-step":
        current++;
        console.log(
          `【==================================开始Loop_${current}_==================================】`,
        );
        break;
        
      case "tool-call":
        console.log("正在调用工具" + chunk.toolName);
        hasToolCall = true; // 本轮有工具调用,代表还要再多一次循环,把调用的结果塞会给大模型多处理一次!
        console.log(
          `【==================================看来还需要多一轮,来处理本轮"${chunk.toolName}"的调用结果==================================】`,
        );
        break;
        
      case "tool-result":
        console.log(
          `【==================================调用工具结果${chunk.output}==================================】`,
        );
        break;
        
      case "text-delta":
        process.stdout.write(chunk.text);
        break;
      case "finish-step":
        console.log();
        console.log(
          "【==================================结束Loop" +
            current +
            "==================================】",
        );
        break;
        
      case "finish":
        // result.response 是一个 Promise,resolve 之后包含这一步的完整信息
        const stepMessages = await result.response;
        console.log(
          "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀当前step的全部信息:",
          JSON.stringify(stepMessages),
        );
        messages.push(...stepMessages.messages);
    }
  }
  if (!hasToolCall) {
    break;
  }
}

查看一下执行日志,我们来盘一盘里面的执行细节

➜  agent-sdy git:(main) ✗ npm run start

> agent-sdy@1.0.0 start
> tsx src/index.ts


 User Say:查询广州深圳上海的天气,并使用数学精准计算温差,并且给出旅行出行的建议
【==================================开始Loop_1_==================================】
正在调用工具get_weather
【==================================看来还需要多一轮,来处理本轮"get_weather"的调用结果==================================】
【==================================调用工具结果小雨,21-32°C,南风 2 级==================================】
正在调用工具get_weather
【==================================看来还需要多一轮,来处理本轮"get_weather"的调用结果==================================】
【==================================调用工具结果阵雨,22-28°C,南风 2 级==================================】
正在调用工具get_weather
【==================================看来还需要多一轮,来处理本轮"get_weather"的调用结果==================================】
【==================================调用工具结果多云,18-22°C,西南风 3 级==================================】

【==================================结束Loop1==================================】
🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀当前step的全部信息: {"id":"chatcmpl-65e914f0-f238-9bb1-8e61-90b04f34414a","timestamp":"2026-05-16T06:49:50.000Z","modelId":"qwen-plus-latest","headers":{"content-type":"text/event-stream;charset=utf-8","date":"Sat, 16 May 2026 06:49:51 GMT","req-arrive-time":"1778914190807","req-cost-time":"884","resp-start-time":"1778914191692","server":"istio-envoy","transfer-encoding":"chunked","vary":"Origin","x-dashscope-call-gateway":"true","x-envoy-upstream-service-time":"884","x-request-id":"65e914f0-f238-9bb1-8e61-90b04f34414a"},"messages":[{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_78b63307a61a4aa7890077","toolName":"get_weather","input":{"city":"广州"}},{"type":"tool-call","toolCallId":"call_16c28b0ea5884d7eb6e01e","toolName":"get_weather","input":{"city":"深圳"}},{"type":"tool-call","toolCallId":"call_f48ced54b4f642ac9b7767","toolName":"get_weather","input":{"city":"上海"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_78b63307a61a4aa7890077","toolName":"get_weather","output":{"type":"text","value":"小雨,21-32°C,南风 2 级"}},{"type":"tool-result","toolCallId":"call_16c28b0ea5884d7eb6e01e","toolName":"get_weather","output":{"type":"text","value":"阵雨,22-28°C,南风 2 级"}},{"type":"tool-result","toolCallId":"call_f48ced54b4f642ac9b7767","toolName":"get_weather","output":{"type":"text","value":"多云,18-22°C,西南风 3 级"}}]}]}


【==================================开始Loop_2_==================================】
正在调用工具calc_math
【==================================看来还需要多一轮,来处理本轮"calc_math"的调用结果==================================】
【==================================调用工具结果32 - 18 = 14==================================】
正在调用工具calc_math
【==================================看来还需要多一轮,来处理本轮"calc_math"的调用结果==================================】
【==================================调用工具结果28 - 22 = 6==================================】
正在调用工具calc_math
【==================================看来还需要多一轮,来处理本轮"calc_math"的调用结果==================================】
【==================================调用工具结果22 - 18 = 4==================================】

【==================================结束Loop2==================================】
🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀当前step的全部信息: {"id":"chatcmpl-4c5dc251-49b6-988c-a6d4-4e3a913f17e6","timestamp":"2026-05-16T06:49:52.000Z","modelId":"qwen-plus-latest","headers":{"content-type":"text/event-stream;charset=utf-8","date":"Sat, 16 May 2026 06:49:53 GMT","req-arrive-time":"1778914192900","req-cost-time":"718","resp-start-time":"1778914193619","server":"istio-envoy","transfer-encoding":"chunked","vary":"Origin","x-dashscope-call-gateway":"true","x-envoy-upstream-service-time":"718","x-request-id":"4c5dc251-49b6-988c-a6d4-4e3a913f17e6"},"messages":[{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_8dcb1b74507a4ab4810a4c","toolName":"calc_math","input":{"expression":"32 - 18"}},{"type":"tool-call","toolCallId":"call_25befd9972d84cff8a070a","toolName":"calc_math","input":{"expression":"28 - 22"}},{"type":"tool-call","toolCallId":"call_2d240856cce94141b8bbfb","toolName":"calc_math","input":{"expression":"22 - 18"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_8dcb1b74507a4ab4810a4c","toolName":"calc_math","output":{"type":"text","value":"32 - 18 = 14"}},{"type":"tool-result","toolCallId":"call_25befd9972d84cff8a070a","toolName":"calc_math","output":{"type":"text","value":"28 - 22 = 6"}},{"type":"tool-result","toolCallId":"call_2d240856cce94141b8bbfb","toolName":"calc_math","output":{"type":"text","value":"22 - 18 = 4"}}]}]}


【==================================开始Loop_3_==================================】
根据查询结果,三地天气及温差分析如下:

>   
> 雨,22–28°C,南风2级  
- **上海**:多云,18–22°C,西南风3级  

**精准温差计算**:
- 广州(最高)与上海(最低)温差:**32 − 18 = 14°C**(最大温差)  
- 深圳(最高)与上海(最低)温差:**28 − 18 = 10°C**(但实际已计算为28−22=6,此处更正:应取各城市自身日温差或跨城对比——按用户要求“温差”理解为城市间最高/最低对比)  
  - 更合理旅行温差参考:  
    - 广州 vs 上海:高温差达 **14°C**,体感差异显著;  
    - 深圳 vs 上海:高温差 **28−18 = 10°C**,低温差 **22−22 = 0°C**;  
    - 广州 vs 深圳:高温差 **32−28 = 4°C**,较接近。

> 
> **:  
>  广州/深圳:轻薄长袖+短袖+便携雨具(小雨/阵雨频发);  
- 上海:需加薄外套或衬衫,早晚微凉(最低18°C),注意防风(西南风3级)。  

✅ **行程安排**:  
- 优先安排上海行程在白天温度较稳时段(10:00–16:00);  
- 广州、深圳建议室内景点与避雨动线结合(如博物馆、商场),并关注短时强降雨预警。  

✅ **健康提示**:  
- 南方湿度大+温差超10°C,易引发感冒,注意及时增减衣物;  
- 上海风力稍大,户外活动注意防晒与防风。

如需生成具体3日行程表或打包清单,可随时告诉我! 🌦️🎒
【==================================结束Loop3==================================】
🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀当前step的全部信息: {"id":"chatcmpl-fefefb00-9441-9a30-97bf-646ce8d76246","timestamp":"2026-05-16T06:49:55.000Z","modelId":"qwen-plus-latest","headers":{"content-type":"text/event-stream;charset=utf-8","date":"Sat, 16 May 2026 06:49:55 GMT","req-arrive-time":"1778914195018","req-cost-time":"491","resp-start-time":"1778914195510","server":"istio-envoy","transfer-encoding":"chunked","vary":"Origin","x-dashscope-call-gateway":"true","x-envoy-upstream-service-time":"490","x-request-id":"fefefb00-9441-9a30-97bf-646ce8d76246"},"messages":[{"role":"assistant","content":[{"type":"text","text":"根据查询结果,三地天气及温差分析如下:\n\n- **广州**:小雨,21–32°C,南风2级  \n- **深圳**:阵雨,22–28°C,南风2级  \n- **上海**:多云,18–22°C,西南风3级  \n\n**精准温差计算**:\n- 广州(最高)与上海(最低)温差:**32 − 18 = 14°C**(最大温差)  \n- 深圳(最高)与上海(最低)温差:**28 − 18 = 10°C**(但实际已计算为28−22=6,此处更正:应取各城市自身日温差或跨城对比——按用户要求“温差”理解为城市间最高/最低对比)  \n  - 更合理旅行温差参考:  \n    - 广州 vs 上海:高温差达 **14°C**,体感差异显著;  \n    - 深圳 vs 上海:高温差 **28−18 = 10°C**,低温差 **22−22 = 0°C**;  \n    - 广州 vs 深圳:高温差 **32−28 = 4°C**,较接近。\n\n**旅行出行建议**:\n✅ **穿衣推荐**:  \n- 广州/深圳:轻薄长袖+短袖+便携雨具(小雨/阵雨频发);  \n- 上海:需加薄外套或衬衫,早晚微凉(最低18°C),注意防风(西南风3级)。  \n\n✅ **行程安排**:  \n- 优先安排上海行程在白天温度较稳时段(10:00–16:00);  \n- 广州、深圳建议室内景点与避雨动线结合(如博物馆、商场),并关注短时强降雨预警。  \n\n✅ **健康提示**:  \n- 南方湿度大+温差超10°C,易引发感冒,注意及时增减衣物;  \n- 上海风力稍大,户外活动注意防晒与防风。\n\n如需生成具体3日行程表或打包清单,可随时告诉我! 🌦️🎒"}]}]}

 User Say:
  1. 定义了工具(description + inputSchema + execute

  2. streamText 加了 tools 参数

  3. fullStream 替代 textStream,处理工具调用事件

  4. 加了一个 while 循环,让模型能够多步执行

但行为上的变化是质的—AI 从"只会说"变成了"能做"。

hasToolCall 表示是否有工具调用

  • 默认为 False 如果当前没有工具调用,那么就不会有工具的执行结果,就不需要下次循环来让大模型处理工具的执行结果了!

  • 如果为有了工具调用,那么内部就需要把本次工具调用的结果放入 message 然后放入到下次的循环调用来处理本次的调用结果!

maxLoop 防止AI幻觉,比如每次AI都要觉得调用工具,死循环下去,那么上下文就会无限扩张填充,我们需要给一个最大值!

其实不难看出,虽然我们用 switch 监听了很多状态,但是其实SDK内部已经帮我们维护好了每轮的调用结果,我们只需要取出来放入自己维护的message就可以了还是很方便的

执行细节:

  1. Loop 1 的时候,查询三个天气的工具都是一次性并发调用完的

  2. 然后 Loop2 拿到结果,发现还是需要调用工具,又是一次性调用完,并且拿到结果填充到 message,进入下次循环

  3. Loop3 已经可以拿到足够的信息给用户输出,不需要调用工具,循环结束了!

这就是 Agent 和 ChatBot 的本质区别。

ChatBot 只会给你建议,有的甚至给你编造答案!

Agent回去规划自己如果想满足你的需求,有哪些工具可以使用,然后根据工具结果分析,是不是要进行下次的调用处理!

这种灵活性是 Agent 区别于传统工作流(workflow)的根本差异。

工作流是写死的流程图——步骤 1 做什么、步骤 2 做什么。

Agent 每一步都在重新评估"当前的目标是什么、我已经有哪些信息、还需要做什么"。

这也是为什么 Agent 适合处理那些你没法提前把流程全部想清楚的任务。

我们可以来对比一下改造前后的代码结构差异,从单次调用变成了循环调用,从只处理文本变成了处理多种事件类型。实际上就是加了一层"决策"——模型每一轮都可以选择"继续调工具"还是"直接回复"。

这个 while 循环就是 Agent 的心脏。

SDK 自动循环

Vercel AI SDK 提供了一个很方便的能力——自动多步执行。当模型返回工具调用时,SDK 会自动执行工具、把结果喂回模型、让模型继续生成,直到模型不再调用工具为止。

控制这个行为的参数叫 stopWhen

但是这里不想多去讲,其实就是SDK自己内部去维护这些结果的类型,然后自己控制循环!但是这样我们就没办法感知到,模型本身干了什么

我们很需要看到循环在干什么。在生产级 Agent 里,一定会自己控制这个循环——因为你需要在每一步之间做很多事:打日志、检查 token 用量、判断是不是陷入死循环、决定要不要中断。