MCP 业务落地实践

纸上得来终觉浅,绝知此事要躬行。在上篇近期LLM的一些趋势之二——MCP之后,很快发现这不只是趋势,而是已经成为行业前仆后继落地的现实。虽然本心是不想为了热点和流行而去追逐,但近期有好几个业务场景在分析和demo之后,都发现可以通过MCP以成本较低、有标准可循且可持续迭代演进的方式显著改善旧有问题的解决效果。因此,这个热点算是一拍即合。

网上关于MCP的文章很多了,但是更多是停留在MCP本身,很多其实一眼可以看出也是AI的产物。这本身是时代发展的趋势,但这些文章都缺乏如何把MCP与LLM结合落地的全局视角。更进一步的,真正落地时候需呀处理的关键细节也是当前缺失的。以前提起躬身入局是一种行动,更是一种姿态的表达,而在AI时代来临面前这可能是留给human的不多的一块自留地。下面以自己实践的过程,结合协议底层拆解下MCP的落地细节和一些关键问题思考。

MCP 协议

MCP RPC 目前发布了两个版本。都可以在官网specification中找到。
* 2024-11-05: https://modelcontextprotocol.io/specification/2024-11-05
* 2025-03-26: https://modelcontextprotocol.io/specification/2025-03-26

官方的文档整体质量很高,我推荐所有要做MCP业务落地的都花一点时间全文阅读。尤其是其中的Best Practice部分,里面有很多非常实用的建议和最佳实践。

  • 当前时间点上,生态上对2024-11-05的支持更加完善,因此生产级别上使用的话,建议先采纳这个版本的实现,兼容性上也会更好。
  • transport 层支持stdio 和 HTTP with SSE(后简称SSE) 两种模式。
  • stdio 适合在本地运行的agent client
  • HTTP with SSE 适合远程运行的agent client
  • 因此,大部分B/S模式的业务落地应该使用 SSE 作为 transport 层。因此MCP 网关是架构上很自然要建设的关键基建之一。下文我们单独讲这部分内容。
  • 两种 transport 均使用 JSON-RPC 2.0 作为消息格式进行通信。这部分内容也强烈建议阅读一下 specification, 对后面无论是落地建设 MCP Server 还是 MCP gateway 都有很大帮助。

如何把构建或将已有服务转换为 MCP Server

这两个问题之所以放在一起,是因为在SSE 作为 transport 的前提下,本质上这是同一个问题。在这种模式下的 MCP Server 是一个 支持 MCP 协议的 HTTP server,负责处理来自 client 的请求,并将结果返回给 client。MCP Server 需要实现以下几个关键点:

  • 主要处理两个端点 endpoint: SSE 连接端点 以及 消息通信端点。
  • SSE连接端点:一般实现使用的端点为 /sse, 该端点负责与client建立长连接session, 走 SSE. 这里可以做鉴权认证,同时会返回给client 消息通信的端点。
  • 消息通信端点:一般实现使用的端点为 /message 进行消息的发送和接收,走标准的 HTTP POST, 这里也可以做鉴权认证。
  • 其他详情可参考参考spec中的(Transport)[https://modelcontextprotocol.io/specification/2024-11-05/basic/transports].

作为一名gopher, 简单看了下mcp-go相关部分的实现:

SSE 端点建立连接,返回消息通信端点:

func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
...
// Send the initial endpoint event
fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", s.GetMessageEndpointForClient(sessionID))
flusher.Flush()

// Main event loop - this runs in the HTTP handler goroutine
for {
select {
case event := <-session.eventQueue:
            // Write the event to the response
            fmt.Fprint(w, event)
            flusher.Flush()
        case <-r.Context().Done():
            close(session.done)
            return
        case <-session.done:
            return
        }
    }

注:其中SSE 消息格式可参考阮一峰老师的Server-Sent Events 教程

消息通信端点HTTP POST请求处理:


    switch baseMessage.Method {
    case mcp.MethodInitialize:
        var request mcp.InitializeRequest
        var result *mcp.InitializeResult
        if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
            err = &requestError{
                id:   baseMessage.ID,
                code: mcp.INVALID_REQUEST,
                err:  &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
            }
        } else {
            s.hooks.beforeInitialize(ctx, baseMessage.ID, &request)
            result, err = s.handleInitialize(ctx, baseMessage.ID, request)
        }
        if err != nil {
            s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
            return err.ToJSONRPCError()
        }
        s.hooks.afterInitialize(ctx, baseMessage.ID, &request, result)
        return createResponse(baseMessage.ID, *result)
    case mcp.MethodPing:
        var request mcp.PingRequest
        var result *mcp.EmptyResult
        if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
            err = &requestError{
                id:   baseMessage.ID,
                code: mcp.INVALID_REQUEST,
                err:  &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
            }
        } else {
            s.hooks.beforePing(ctx, baseMessage.ID, &request)
            result, err = s.handlePing(ctx, baseMessage.ID, request)
        }
        if err != nil {
            s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err)
            return err.ToJSONRPCError()
        }
        s.hooks.afterPing(ctx, baseMessage.ID, &request, result)
        return createResponse(baseMessage.ID, *result)
    case mcp.MethodResourcesList:

从上面代码可以看出,无论是初始化、接口列表发现、还是工具调用,都是通过HTTP POST请求来实现的。这里需要注意的是,MCP Server 需要同时支持长连接的SSE端点和短连接的HTTP POST端点。

但是在实际的业务系统中,不太可能把每个应用服务都按照以上方式进行改造。一个推荐的架构方式:

MCP client –> MCP server 网关 –> application server

但是从效果上看,以为直接套用这个架构思路就可以大杀四方就是一种幻想了。从实践上看,至少要做好一下节点:

  • 如何进行领域MCP Server的划分。映射到tools上,就是如何做接口的领域分层和治理。
  • tools中每个接口到应用层接口的编排
  • 每个tool的准确描述、参数说明,以及返回参数的剪裁,确保值返回必要、明确的字段。
  • 接口返回结果错误的精细处理和返回。这部分需要一些耐心,确保LLM能够尽可能的理解异常情况,持续react, 而不阻断后续推理。

如何在agent client中使用MCP

MCP Client 作为桥梁,按照协议与 MCP Server 进行通信。然后将MCP server 提供的tools, resources, prompts 提供给LLM。

实际应用中,LLM 通过prompt发现和使用mcp server:

{
"model": "deepseek-reasoner",
"messages": [
{
"role": "system",
"content": "In this environment you have access to a set of tools you can use to answer the user's question. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.\n\n## Tool Use Formatting\n\nTool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:\n\n<tool_use>\n  <name>{tool_name}</name>\n  <arguments>{json_arguments}</arguments>\n</tool_use>\n\nThe tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:\n<tool_use>\n  <name>python_interpreter</name>\n  <arguments>{\"code\": \"5 + 3 + 1294.678\"}</arguments>\n</tool_use>\n\nThe user will respond with the result of the tool use, which should be formatted as follows:\n\n<tool_use_result>\n  <name>{tool_name}</name>\n  <result>{result}</result>\n</tool_use_result>\n\nThe result should be a string, which can represent a file or any other output type. You can use this result as input for the next action.\nFor example, if the result of the tool use is an image file, you can use it in the next action like this:\n\n<tool_use>\n  <name>image_transformer</name>\n  <arguments>{\"image\": \"image_1.jpg\"}</arguments>\n</tool_use>\n\nAlways adhere to this format for the tool use to ensure proper parsing and execution.\n\n## Tool Use Examples\n\nHere are a few examples using notional tools:\n---\nUser: Generate an image of the oldest person in this document.\n\nAssistant: I can use the document_qa tool to find out who the oldest person is in the document.\n<tool_use>\n  <name>document_qa</name>\n  <arguments>{\"document\": \"document.pdf\", \"question\": \"Who is the oldest person mentioned?\"}</arguments>\n</tool_use>\n\nUser: <tool_use_result>\n  <name>document_qa</name>\n  <result>John Doe, a 55 year old lumberjack living in Newfoundland.</result>\n</tool_use_result>\n\nAssistant: I can use the image_generator tool to create a portrait of John Doe.\n<tool_use>\n  <name>image_generator</name>\n  <arguments>{\"prompt\": \"A portrait of John Doe, a 55-year-old man living in Canada.\"}</arguments>\n</tool_use>\n\nUser: <tool_use_result>\n  <name>image_generator</name>\n  <result>image.png</result>\n</tool_use_result>\n\nAssistant: the image is generated as image.png\n\n---\nUser: \"What is the result of the following operation: 5 + 3 + 1294.678?\"\n\nAssistant: I can use the python_interpreter tool to calculate the result of the operation.\n<tool_use>\n  <name>python_interpreter</name>\n  <arguments>{\"code\": \"5 + 3 + 1294.678\"}</arguments>\n</tool_use>\n\nUser: <tool_use_result>\n  <name>python_interpreter</name>\n  <result>1302.678</result>\n</tool_use_result>\n\nAssistant: The result of the operation is 1302.678.\n\n---\nUser: \"Which city has the highest population , Guangzhou or Shanghai?\"\n\nAssistant: I can use the search tool to find the population of Guangzhou.\n<tool_use>\n  <name>search</name>\n  <arguments>{\"query\": \"Population Guangzhou\"}</arguments>\n</tool_use>\n\nUser: <tool_use_result>\n  <name>search</name>\n  <result>Guangzhou has a population of 15 million inhabitants as of 2021.</result>\n</tool_use_result>\n\nAssistant: I can use the search tool to find the population of Shanghai.\n<tool_use>\n  <name>search</name>\n  <arguments>{\"query\": \"Population Shanghai\"}</arguments>\n</tool_use>\n\nUser: <tool_use_result>\n  <name>search</name>\n  <result>26 million (2019)</result>\n</tool_use_result>\nAssistant: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.\n\n\n## Tool Use Available Tools\nAbove example were using notional tools that might not exist for you. You only have access to these tools:\n<tools>\n\n<tool>\n  <name>fj5EiLHfjeWQhDMmQTXZUr</name>\n  <description>获取当前时间</description>\n  <arguments>\n    {\"type\":\"object\"}\n  </arguments>\n</tool>\n\n\n<tool>\n  <name>fq-YVmKZQ2p6aciTm5WVtX</name>\n  <description>获取技能组指标的当前值</description>\n  <arguments>\n    {\"type\":\"object\",\"properties\":{\"qualifierCodeList\":{\"description\":\"指标code列表\",\"type\":\"array\"},\"skillGroupId\":{\"description\":\"技能组ID\",\"type\":\"string\"}},\"required\":[\"skillGroupId\",\"qualifierCodeList\"]}\n  </arguments>\n</tool>\n\n\n<tool>\n  <name>fTlRF3f4Q22VGUdO0xbIxt</name>\n  <description>获取技能组指标的历史值</description>\n  <arguments>\n    {\"type\":\"object\",\"properties\":{\"beginTime\":{\"description\":\"开始时间,格式:2024-11-28 18:00:00,必须为整分钟\",\"type\":\"string\"},\"endTime\":{\"description\":\"结束时间,格式:2024-11-28 18:00:00,必须为整分钟\",\"type\":\"string\"},\"qualifierCode\":{\"description\":\"指标code\",\"type\":\"string\"},\"skillGroupId\":{\"description\":\"技能组ID\",\"type\":\"string\"}},\"required\":[\"skillGroupId\",\"qualifierCode\",\"beginTime\",\"endTime\"]}\n  </arguments>\n</tool>\n\n\n<tool>\n  <name>fU6szvcmSkfRaFBoMbptOz</name>\n  <description>获取技能组的指标列表</description>\n  <arguments>\n    {\"type\":\"object\"}\n  </arguments>\n</tool>\n\n</tools>\n\n## Tool Use Rules\nHere are the rules you should always follow to solve your task:\n1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.\n2. Call a tool only when needed: do not call the search agent if you do not need information, try to solve the task yourself.\n3. If no tool call is needed, just answer the question directly.\n4. Never re-do a tool call that you previously did with the exact same parameters.\n5. For tool use, MARK SURE use XML tag format as shown in the examples above. Do not use any other format.\n\n# User Instructions\n\n\nNow Begin! If you solve the task correctly, you will receive a reward of $1,000,000.\n"
},
{
"role": "user",
"content": "分析一下饿了么技能组 1221000000006593548 今天18点以来的平均产能趋势,用表格展示\n"
}
],
"stream": true
}

系统prompt部分格式化后如下:

In this environment you have access to a set of tools you can use to answer the user's question. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.

## Tool Use Formatting

Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:

<tool_use>
<name>{tool_name}</name>
<arguments>{json_arguments}</arguments>
</tool_use>

The tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:
<tool_use>
<name>python_interpreter</name>
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
</tool_use>

The user will respond with the result of the tool use, which should be formatted as follows:

<tool_use_result>
<name>{tool_name}</name>
<result>{result}</result>
</tool_use_result>

The result should be a string, which can represent a file or any other output type. You can use this result as input for the next action.
For example, if the result of the tool use is an image file, you can use it in the next action like this:

<tool_use>
<name>image_transformer</name>
<arguments>{"image": "image_1.jpg"}</arguments>
</tool_use>

Always adhere to this format for the tool use to ensure proper parsing and execution.

## Tool Use Examples

Here are a few examples using notional tools:
---
User: Generate an image of the oldest person in this document.

Assistant: I can use the document_qa tool to find out who the oldest person is in the document.
<tool_use>
<name>document_qa</name>
<arguments>{"document": "document.pdf", "question": "Who is the oldest person mentioned?"}</arguments>
</tool_use>

User: <tool_use_result>
<name>document_qa</name>
<result>John Doe, a 55 year old lumberjack living in Newfoundland.</result>
</tool_use_result>

Assistant: I can use the image_generator tool to create a portrait of John Doe.
<tool_use>
<name>image_generator</name>
<arguments>{"prompt": "A portrait of John Doe, a 55-year-old man living in Canada."}</arguments>
</tool_use>

User: <tool_use_result>
<name>image_generator</name>
<result>image.png</result>
</tool_use_result>

Assistant: the image is generated as image.png

---
User: "What is the result of the following operation: 5 + 3 + 1294.678?"

Assistant: I can use the python_interpreter tool to calculate the result of the operation.
<tool_use>
<name>python_interpreter</name>
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
</tool_use>

User: <tool_use_result>
<name>python_interpreter</name>
<result>1302.678</result>
</tool_use_result>

Assistant: The result of the operation is 1302.678.

---
User: "Which city has the highest population , Guangzhou or Shanghai?"

Assistant: I can use the search tool to find the population of Guangzhou.
<tool_use>
<name>search</name>
<arguments>{"query": "Population Guangzhou"}</arguments>
</tool_use>

User: <tool_use_result>
<name>search</name>
<result>Guangzhou has a population of 15 million inhabitants as of 2021.</result>
</tool_use_result>

Assistant: I can use the search tool to find the population of Shanghai.
<tool_use>
<name>search</name>
<arguments>{"query": "Population Shanghai"}</arguments>
</tool_use>

User: <tool_use_result>
<name>search</name>
<result>26 million (2019)</result>
</tool_use_result>
Assistant: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.

## Tool Use Available Tools
Above example were using notional tools that might not exist for you. You only have access to these tools:
<tools></tools>

<tool>
<name>fj5EiLHfjeWQhDMmQTXZUr</name>
<description>获取当前时间</description>
<arguments>
{"type":"object"}
</arguments>
</tool>

<tool>
<name>fq-YVmKZQ2p6aciTm5WVtX</name>
<description>获取技能组指标的当前值</description>
<arguments>
{"type":"object","properties":{"qualifierCodeList":{"description":"指标code列表","type":"array"},"skillGroupId":{"description":"技能组ID","type":"string"}},"required":["skillGroupId","qualifierCodeList"]}
</arguments>
</tool>

<tool>
<name>fTlRF3f4Q22VGUdO0xbIxt</name>
<description>获取技能组指标的历史值</description>
<arguments>
{"type":"object","properties":{"beginTime":{"description":"开始时间,格式:2024-11-28 18:00:00,必须为整分钟","type":"string"},"endTime":{"description":"结束时间,格式:2024-11-28 18:00:00,必须为整分钟","type":"string"},"qualifierCode":{"description":"指标code","type":"string"},"skillGroupId":{"description":"技能组ID","type":"string"}},"required":["skillGroupId","qualifierCode","beginTime","endTime"]}
</arguments>
</tool>

<tool>
<name>fU6szvcmSkfRaFBoMbptOz</name>
<description>获取技能组的指标列表</description>
<arguments>
{"type":"object"}
</arguments>
</tool>

## Tool Use Rules
Here are the rules you should always follow to solve your task:
1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.
2. Call a tool only when needed: do not call the search agent if you do not need information, try to solve the task yourself.
3. If no tool call is needed, just answer the question directly.
4. Never re-do a tool call that you previously did with the exact same parameters.
5. For tool use, MARK SURE use XML tag format as shown in the examples above. Do not use any other format.

# User Instructions

Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.

当然最简单的方式就是直接使用支持MCP的客户端应用,比如 Cursor、Cherry Studio.

一些工具

  • MCP Server调试工具:(MCP inspector)[https://github.com/modelcontextprotocol/inspector]
  • Cherry Studio:很灵活、用户体验很好的支持MCP 的 agent client.

近期LLM的一些趋势之二——MCP

书接上回,文末提到一个点:

当然也有不变的点:工具的建设依然很重要。只不过,今年需要更多的思考如何将这些基建工具与上述的flowgraph结合起来。

当时没有展开写,而最近随着MCP(Managed Context Protocol)的逐步普及和应用,今天来补一下当时挖的坑。

什么是MCP

参考MCP的官方文档,MCP是一个用于管理和使用上下文的协议,旨在帮助开发者更好地与大型语言模型(LLM)进行交互。它提供了一种结构化的方法来处理上下文信息,使得在与LLM进行对话时,可以更有效地传递和管理信息。

它的工作流程也很简单(LLM以Claude为例):

When you submit a query:

  • The client gets the list of available tools from the server
  • Your query is sent to Claude along with tool descriptions
  • Claude decides which tools (if any) to use
  • The client executes any requested tool calls through the server
  • Results are sent back to Claude
  • Claude provides a natural language response
  • The response is displayed to you

从工作流程来看,是不是感觉跟我们熟悉的Function Calling有些类似?

MCP vs function calling

MCP和Function Calling的区别在于:

  1. 功能范围:MCP不仅仅是一个函数调用的接口,它还包括了上下文管理、工具描述和结果处理等多个方面的功能。而Function Calling主要集中在函数调用本身。
  2. 上下文管理:MCP提供了更为全面的上下文管理功能,可以在多个工具之间共享和传递上下文信息。而Function Calling通常是针对单个函数的调用,缺乏跨函数的上下文管理能力。
  3. 工具描述:MCP允许开发者为每个工具提供详细的描述信息,以便LLM更好地理解如何使用这些工具。而Function Calling通常只提供函数名和参数,缺乏详细的描述信息。
  4. 结果处理:MCP在结果处理上也提供了更多的灵活性,可以根据不同的工具和上下文信息进行定制化的结果处理。而Function Calling通常是固定的结果处理方式,缺乏灵活性。
  5. 适用场景:MCP更适合于复杂的应用场景,尤其是需要多个工具协同工作的场景。而Function Calling更适合于简单的函数调用场景。
  6. 扩展性:MCP的设计考虑了未来的扩展性,可以方便地添加新的工具和功能。而Function Calling在扩展性上相对较弱,添加新功能可能需要较大的改动。
  7. 社区支持:MCP是一个开放的协议,得到了广泛的社区支持和参与。而Function Calling通常是由特定的公司或组织维护,缺乏广泛的社区支持。
  8. 文档和示例:MCP提供了详细的文档和示例,帮助开发者快速上手。而Function Calling的文档和示例相对较少,可能需要开发者自行摸索。
  9. 学习曲线:由于MCP的功能更为全面和复杂,学习曲线相对较陡。而Function Calling相对简单,学习曲线较平缓。
  10. 性能:在性能方面,MCP可能会因为其复杂性而引入一定的性能开销。而Function Calling通常是直接调用函数,性能较高。
  11. 安全性:MCP在设计上考虑了安全性,提供了一些安全机制来保护上下文信息。而Function Calling通常缺乏这样的安全机制,可能存在一定的安全隐患。
  12. 兼容性:MCP是一个开放的协议,可以与多种语言和平台兼容。而Function Calling通常是针对特定语言或平台的,兼容性较差。

此外,可以参考《MCP 与 Function Call 区别》。其中提到一个比喻非常形象:

  • MCP:通用协议层面的标准约定,就像给 LLM 使用的“USB-C规范”。
  • Function Call:特定大模型厂商提供的独特特性,就像某品牌手机的专属充电协议。

如何使用?

回到我们最开始的观点:不变的点:工具的建设依然很重要。对于MCP来说,主要是指MCP Server。作为一个gopher,推荐者以下两个项目:

两个项目都是golang传统的开箱即用。

要让LLM能够把提供工具、资源、prompt驱动起来,需要借助 MCP client 这个桥梁。而目前从官方推荐的Clients来看,MCP client 主要以两种形式出现:①面向普通用户的application应用,如Claude Desktop App;②面向开发者的agent框架库,如mcp-agent。这里推荐fast-agent.

MCP 的意义

MCP的意义在于它为开发者提供了一种结构化的方法来管理和使用上下文信息,使得与LLM的交互更加高效和灵活。通过MCP,开发者可以更好地利用LLM的能力,构建出更为复杂和智能的应用程序。

还是有点抽象,对不对?从过去技术发展历史来看,MCP跟当年的微服务架构有点类似。不同的是,微服务架构下更多的还是工程、业务逻辑驱动数据流和控制流,而MCP则是将LLM作为决策中枢。也就是说,MCP是一个面向LLM的微服务架构。

太阳底下没有新鲜事,以前微服务架构下的经验在MCP架构下会有一定的借鉴意义。另一方面,MCP的出现也意味着我们需要重新审视和设计我们的应用架构,以更好地适应LLM的特点和需求。

近期LLM的一些趋势

不知不觉,LLM已经高歌猛进到了第四年。依然清晰记得两年前使用ChatGPT时候的震撼。那个时候大家认为提出问题是非常重要的能力,这里面也包括prompt优化。很多人的第一次震撼应该大多来自于提出一个好问题后LLM给的超出预期的反馈。

两年过去了,以上提到的依然重要。但是LLM也展示了一些新的魅力。

首先是菜市场大妈都耳熟梦想的DeepSeek R1。用刘飞的总结就是“开城墙”:MIT最友好开源协议,数量级降低训练成本,纯强化学习。分析DeepSeek的文章很多,这里主要是为了引出后面的两个应用。

第一个应用是 OpenAI 的 Operator:

Operator 通过“观察”(通过屏幕截图)和“交互”(使用鼠标和键盘的所有操作)与浏览器进行通信,使其无需定制 API 集成即可在 Web 上执行操作。

简单总结:

  • 感知:文本+截图
  • 交互:确定点击位置
  • 思考决策:GPT-4o 多模态

第二个应用来自字节的UI-TARS

UR-TARS 是该系统系统视觉大模型,执行可以通过 UI-TARS desktop (支撑各种应用程序)以及Midscene.js(仅浏览器)。

UI-TARS is a next-generation native GUI agent model designed to interact seamlessly with graphical user interfaces (GUIs) using human-like perception, reasoning, and action capabilities.

Unlike traditional modular frameworks, UI-TARS integrates all key components—perception, reasoning, grounding, and memory—within a single vision-language model (VLM), enabling end-to-end task automation without predefined workflows or manual rules.

以上三个看似LLM不同方向的进展其实背后都在强调一个概念:推理。从效果看,也就是大众口中的端到端。而推理本质上其实就是让大模型能够的进行自主planning。

在Lillian的范式:agent = LLM + planning + memory + tools中,有过落地经验的就会发现,最重要也最挑战的就是planning。解决planning无论是形式上还是概念是都很多,比如思维链、思维树、反思、SOP等,但其本质上都是为了让LLM去follow某个flowgraph以完成任务。

很多应用场景中使用LLM落地,其实就是围绕如何产出这个flowgraph。比如面向普通用户的可视化画布,面向power user的编码式SOP。而一旦这个flowgraph的产出需要依赖人工参与,那么其可泛化性一定是有限的,再结合成本问题,往往让很多应用场景要么铩羽而归,要么看起来是为了用而用。

而LLM基本都在相同时间专项推理的时候,大家发现flowgraph很多时候可以是LLM内置的。这个时候,LLM的应用场景就会有一个质的飞跃。比如Operator和UI-TARS,都是在这个方向上的尝试。而这是今年区别于两年前很重要的一个跃迁。

因此,今年进行LLM场景落地不妨考虑这两个思路:

  • 对于容错性高的场景,解决思路多想想怎么激发LLM内部的这个flowgraph。
  • 对于容错性低的场景,尝试挖掘LLM的内在flowgraph,然后结合人工的审核再应用。

当然也有不变的点:工具的建设依然很重要。只不过,今年需要更多的思考如何将这些基建工具与上述的flowgraph结合起来。