🔌 第9章 基于MCPServer的生活类智能体应用
📄 一句话概括

「附近」必须先变成一个数:坐标加半径。城市名只能给出一个城,给不出一个圈。 按距离排还是按评分排,结果的第一名不一样——这不是程序的问题,是用户没说清。 三个坑:外地结果、单位、去重。都不是协议问题,是数据问题。

📝 本章和前面几章的关系

上一节的参数是「城市」这一个词,这一节的参数是四个数:经度、纬度、半径、类型。 参数一多,就出现一个新问题:这些数从哪儿来。 这一节就讲这个,顺便讲清楚「排序」该由谁做。

一、「附近」要落成四个参数

用户说「附近」,模型不知道你在哪。所以工具必须收坐标:经度、纬度。 坐标从哪来?三个来源:宿主从系统定位拿、用户自己输入、或者从一句「我在西湖边」里推。 半径是第三个参数。模型按语境填:说「走两步就到」给三百米,说「开车十分钟」给三公里。

类型是第四个。「餐厅」这个词直接映射过去,但「随便吃点」映射不了,得问。 四个参数里最难的是半径:填大了结果一堆挑不过来,填小了经常一条都没有。 所以好一点的工具会给个默认值,比如一千米。

二、返回里必须带距离

工具返回餐厅列表时,每一家都要带一个「距离多少米」。 这个字段不是给用户看的,是给排序用的。 没有它,模型只能按列表顺序说,而那通常是数据库顺序。 演示里两百米内两家:外婆家 508 米,新白鹿 1699 米。

所以「按最近的推荐」这句话能不能落地,取决于工具返不返回距离。 这又是一次「返回结构决定能力」:第 8 章那个 Figma 工具也是这个道理。

三、排序该由谁做

演示里按距离排第一名是外婆家(508 米,4.3 分),按评分排第一名是新白鹿(1699 米,4.6 分)。 两种排法都「对」。差别在用户没说清要哪个。 好一点的做法是让工具返回原始数据,排序交给模型:它在上下文里能看到「带着老人」这种话, 而程序看不到。

但代价是模型排序可能算错。 所以更好的做法是两者都给:工具给距离和评分,提示词里写清「默认按距离,用户提到评分时按评分」。 这条经验在七个案例里通用:工具给数,规则写在提示词里。

四、三个坑

第一个:结果里有外地的。演示里那家北京的老字号,坐标差着一千公里,但名字很像本地的。 必须按距离过滤,不能按名字过滤。 第二个:半径单位。有的接口给米,有的给公里,传错了差一千倍。schema 里把单位写清楚。 第三个:去重。同一个品牌三家分店,按名字去重就只剩一家。要按 id 去重。

三条都不是协议问题,是数据问题。而这三个坑在真机上都会遇到。 教材里这一节是在平台上配高德 MCP 做的(界面操作本沙箱不执行)。 判据是:问一句「附近有什么吃的」,回来的第一屏带距离。

五、动手 Demo:附近餐厅:半径、排序、去重

一个工具收坐标、半径、类型,返回带距离的餐厅列表。 按距离和按评分各排一遍,看第一名怎么变。 三个坑在演示里各露一次脸:外地结果、单位、重名。

s = Server('map-mcp')
s.add(Tool('restaurant.nearby', '按坐标和半径找餐厅,返回距离(米)',
           schema({'lng': prop('number', '经度'),
                   'lat': prop('number', '纬度'),
                   'radius': dict(prop('integer', '半径,米'),
                                  minimum=100, maximum=5000)},
                  ['lng', 'lat']), fn))

r = call(s, 'restaurant.nearby', {'lng': 120.1551, 'lat': 30.2741,
                                   'radius': 2000})['items']
print(sorted(r, key=lambda x: x['meter'])[0])   # 按距离
print(sorted(r, key=lambda x: -x['rating'])[0])  # 按评分

实跑输出:

--------------------------------------------------------------------
附近餐厅:半径、排序、去重
--------------------------------------------------------------------


  ① 「附近」要先变成一个数
  --------------------------------------------------------------------
模型不知道你在哪,它只知道你说了「附近」。所以工具必须收坐标:

| 参数 | 谁给 | 没有会怎样 |
|---|---|---|
| 经度、纬度 | 宿主从定位或用户输入拿 | 只能按城市猜,不能按距离排 |
| 半径 | 模型按语境填,默认 1000 米 | 结果忽多忽少 |
| 类型 | 模型按「餐厅」这类词填 | 会混进景点 |

  【坐标这一步是「附近」唯一能落地的方式】。别的说法都不行——城市名只能给出一个城,给不出一个圈。

  ② 一个工具,返回带距离的结果
  --------------------------------------------------------------------

| 餐厅 | 距离 | 评分 | 人均 |
|---|---:|---:|---:|
| 外婆家 | 508 米 | 4.3 | 65 元 |
| 新白鹿 | 1699 米 | 4.6 | 78 元 |

  半径 2000 米,命中 2 家。【半径传得越大,模型越难挑】——所以别一上来给一万米。

  ③ 按距离排还是按评分排
  --------------------------------------------------------------------

| 排序依据 | 第一名 | 第一名评分 | 第一名距离 |
|---|---|---:|---:|
| 距离 | 外婆家 | 4.3 | 508 米 |
| 评分 | 新白鹿 | 4.6 | 1699 米 |

  两种排法第一名不一样。【这不是程序的问题,是用户没说清要哪个。】
  好一点的做法是让工具返回原始数据,排序交给模型——它在上下文里能看到
  「用户带着老人」这种话,而程序看不到。

  ④ 三个坑
  --------------------------------------------------------------------
第一个:结果里有外地的。演示里那家北京的老字号,坐标差着一千公里,但名字很像本地的。
【必须按距离过滤,不能按名字过滤。】
第二个:半径单位。有的接口给米,有的给公里,传错了差一千倍。【schema 里写清单位。】
第三个:去重。同一个品牌三家分店,按名字去重就只剩一家。【要按 id 去重。】
  三条都不是协议问题,是数据问题。【而这三个坑在真机上都会遇到。】

①第一张表三行讲参数从哪来。 第二行「半径」写的是「模型按语境填,默认一千米」——这就是提示词该管的事。 ②返回的两家都带距离。距离不是给用户看的,是给排序用的。 ③第三段两种排法第一名不同。这不是 bug,是需求没说清。 ④第四段三个坑,第三个(去重)在演示里没露脸,因为数据不够。没露脸不等于不存在。 ⑤最后那句判据很实用:问一句「附近有什么吃的」,回来的第一屏带距离,就说明这条链通了。

30 秒实验

  1. 把半径从 2000 改成 500,看还剩几家。一家都没有时该怎么答。
  2. 给工具加一个「人均」字段,让「便宜点」这句话能落地。
  3. 想想「按名字去重」会把三家分店并成什么样。

常见误区

  1. 「说城市就够了。」城市给不出一个圈。要有距离,就得有坐标和半径。
  2. 「排序应该工具做。」工具给数,规则写在提示词里。程序看不到「带着老人」这种话。
  3. 「名字一样就是同一家。」同一个品牌的分店名字都一样。要去重得按 id。
自测三问(点开对答案) 1. 「附近」要落成哪几个参数? —— 经度、纬度、半径,再加一个类型。坐标定位置,半径定范围。 2. 为什么返回里必须带距离? —— 距离是排序的依据。不带距离,「推荐最近的」这句话就没法落地。 3. 按名字去重会出什么问题? —— 同一个品牌的分店被并成一家。去重要按 id,不能按名字。

参数从定位来,算顺利。下一个案例的参数从「申请」来——先要一个 Key,才谈得上查。

相关笔记