大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(上)

引子

话说京城近日不太平,东厂的番子们总想着搞些 "天外飞仙" 式的新奇暗器,咱们的大内密探零零发 ------ 这位放着琴操姑娘的柔情不管、专爱捣鼓发明的主儿,哪能坐视不管?

这不,他把 "要你命三千" 的零件往旁边一扔,一头扎进了 iOS 开发的 "密探工具箱",誓要做个能查发明状态、能跟军师聊天的神器。

在本篇京城八卦中,您将学到如下内容:

  • 引子
  • 🛠️ 第一回:立规矩!MCP 服务器的 "武功心法"
  • 📡 第二回:造 "发明检测器"!查状态全靠它
  • 🗣️ 第三回:找个 "军师"!Anthropic API 当参谋
  • 📱 第四回:搭 "聊天棚"!MCP 客户端的门面
  • 🎬 尾声:神器初成,就差最后一步!

毕竟对付坏人,光靠蛮力可不够,还得懂点 Swift 代码,这才叫 "文武双全" 嘛!:)


🛠️ 第一回:立规矩!MCP 服务器的 "武功心法"

零零发常说:"做发明跟练武功一个理,得先有招式框架,不然就是花拳绣腿。" 要做这密探神器,第一步就得给 "MCP 服务器" 立个规矩 ------ 也就是定义个协议,规定它得会两样本事:一是有能用的 "工具列表",二是能 "调用工具干活"。

咱们先看看这 "武功心法" 的代码模样,零零发在草稿纸上是这么写的:

swift 复制代码
import Foundation

// 定义MCP服务器的核心协议(相当于武功秘籍的总纲)
// 任何想当MCP服务器的"发明",都得遵守这规矩
protocol MCPServerProtocol {
    // 必须有一套能用的工具(比如查发明、测暗器的工具)
    var tools: [Tool] { get }
    // 必须能调用工具,还得是异步的(毕竟查发明得等一会儿)
    func call(_ tool: Tool) async throws -> String
}

// 工具结构体(相当于武功里的"招式细节")
// 得告诉别人这工具叫啥、能干啥、要啥参数
struct Tool: Encodable {
    // 编码用的Key,跟服务器约定好叫啥名
    enum CodingKeys: String, CodingKey {
        case name, toolDescription = "description", input_schema
    }
    
    let name: String // 工具名(比如"查最新发明")
    let toolDescription: String // 工具功能说明
    let input_schema: [String: String] // 输入参数格式(必须有,哪怕不用)
}

写完后,零零发一拍桌子说到:"瞧见没?这协议就像'大内密探行为准则',你想当服务器,就得按这来!不然你那工具连名字都没有,跟东厂番子没带腰牌一样 ------ 谁认得你啊?"

📡 第二回:造 "发明检测器"!查状态全靠它

规矩立好了,接下来就得造个真能用的 "服务器"------ 零零发给它起名叫LingLingFaInventionService,专门查最新的密探发明状态,比如 "天外飞仙破解器" 的电量、校准情况。

毕竟这破解器要是没电了,遇到无相皇可就麻烦了!

咱们先看看这 "发明检测器" 的代码,零零发还贴心加了注释:

swift 复制代码
// 密探发明服务(真正的MCP服务器实现,相当于"要你命三千"的核心部件)
final class LingLingFaInventionService: MCPServerProtocol {
    // 给服务器装工具:这里就一个------查最新发明状态
    var tools: [Tool] = [
        Tool(
            name: "check_latest_invention", // 工具名:查最新发明
            toolDescription: "获取密探最新发明状态(如天外飞仙破解器的电量、校准情况)from 密探发明库。", // 功能说明
            input_schema: ["type": "object"] // 输入参数格式(这里不用参数,但按规矩得写)
        )
    ]
    
    // 懒加载"密探发明库"(相当于存发明数据的仓库,类似HealthKit)
    private lazy var inventionStore = LingLingFaInventionStore()
    
    // 调用工具的核心方法:别人叫工具,就走这儿
    func call(_ tool: Tool) async throws -> String {
        // 先判断是不是咱们认识的工具,不是就扔错(总不能让它查东厂的暗器吧)
        guard tool.name == "check_latest_invention" else {
            throw InventionError.toolNotSupported // 自定义错误:工具不支持
        }
        // 调用方法查最新发明状态,返回结果
        let inventionStatus = try await fetchLatestInventionStatus()
        return inventionStatus
    }
    
    // 真正查发明状态的方法(核心逻辑,相当于给"破解器"读数据)
    private func fetchLatestInventionStatus() async throws -> String {
        // 第一步:请求"密探发明库"的授权(不然没权限查数据,跟进宫要腰牌一样)
        try await inventionStore.requestAuthorization(toShare: [], read: [.latestInvention])
        
        // 第二步:查最新的发明数据(按条件查,只找最新的那条)
        let descriptor = InventionQueryDescriptor(
            predicates: [.sample(type: .latestInvention)], // 条件:最新发明
            sortDescriptors: [] // 排序:不用排,直接要最新的
        )
        // 从发明库拿数据
        let samples = try await descriptor.result(for: inventionStore)
        
        // 第三步:判断有没有数据,没有就扔错(总不能查个空气吧)
        guard let sample = samples.first as? InventionSample else {
            throw InventionError.missingInventionData // 自定义错误:没找到发明数据
        }
        
        // 第四步:提取数据(比如电量、校准状态),组装成字符串返回
        let deviceName = sample.deviceName // 发明名:天外飞仙破解器
        let battery = sample.batteryPercentage // 电量:80%
        let isCalibrated = sample.isCalibrated // 校准状态:已校准
        return "\(deviceName):电量\(battery)%,状态:\(isCalibrated ? "已校准" : "未校准")"
    }
}

// 自定义的"密探发明库"(模拟用,相当于HealthKit的替代品)
class LingLingFaInventionStore {
    // 请求授权的方法(模拟)
    func requestAuthorization(toShare: [InventionType], read: [InventionType]) async throws {
        // 实际开发里这里要弹授权框,现在先模拟"授权通过"
        print("密探发明库授权通过!零零发:'搞定!'")
    }
}

// 模拟的查询描述符(相当于查数据的"命令符")
struct InventionQueryDescriptor {
    let predicates: [InventionPredicate]
    let sortDescriptors: [NSSortDescriptor]
    
    // 模拟拿数据的方法
    func result(for store: LingLingFaInventionStore) async throws -> [Any] {
        // 模拟返回一条数据:天外飞仙破解器,电量80%,已校准
        return [InventionSample(
            deviceName: "天外飞仙破解器",
            batteryPercentage: 80,
            isCalibrated: true
        )]
    }
}

// 模拟的发明数据模型
struct InventionSample {
    let deviceName: String
    let batteryPercentage: Int
    let isCalibrated: Bool
}

// 自定义错误(清晰明了,不然报错都不知道哪儿错了)
enum InventionError: Error {
    case toolNotSupported // 工具不支持
    case missingInventionData // 没找到发明数据
}

// 模拟的发明类型和条件(凑齐逻辑用)
enum InventionType { case latestInvention }
struct InventionPredicate { static func sample(type: InventionType) -> Self { Self() } }

零零发还补了句小贴士:"哎我说,想在模拟器测这功能?你得先打开'密探工具箱'APP,在'发明库'里加条测试数据 ------ 就像我给'要你命三千'装刀片一样,没零件它咋干活?

对了,真要上真机,还得在 Info.plist 里加'密探发明库使用说明',不然系统不让你用,跟东厂查腰牌一样严!"

🗣️ 第三回:找个 "军师"!Anthropic API 当参谋

光能查发明还不够,零零发想让神器能 "聊天"------ 比如问一句 "我那天外飞仙破解器弄得咋样了?",就有个 "军师" 帮着分析,还能自动调用工具查。

他找的 "军师" 就是 Anthropic 的 Claude LLM,用 API 连上去,相当于给神器装了个 "脑子"。

这 "军师接口" 的代码,零零发是这么写的:

swift 复制代码
import Foundation

// 军师服务(对接Anthropic API,相当于给神器找个参谋)
final class ClaudeAdvisorService {
    private let apiKey: String // 军师的"通关文牒"(API Key,得从Anthropic官网拿)
    private let tools: [Tool] // 告诉军师咱们有啥工具(比如查发明的工具)
    
    // 初始化:得给通关文牒和工具列表
    init(apiKey: String, tools: [Tool]) {
        self.apiKey = apiKey
        self.tools = tools
    }
    
    // 给军师发消息的方法(核心:把密探的话传给军师,等回复)
    func send(messages: [Request.Message]) async throws -> Response {
        // 1. 确定军师的地址(API URL)
        guard let url = URL(string: "https://api.anthropic.com/v1/messages") else {
            throw AdvisorError.invalidURL // 错误:地址不对
        }
        var request = URLRequest(url: url)
        
        // 2. 装消息头:告诉军师是谁、用啥版本的规矩
        request.httpMethod = "POST" // 用POST方法发消息(跟递奏折一样)
        request.setValue(apiKey, forHTTPHeaderField: "x-api-key") // 通关文牒
        request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version") // 规矩版本
        
        // 3. 装消息体:告诉军师用啥模型、说啥话、有啥工具
        let body = Request(
            model: "claude-3-opus-20240229", // 军师的"脑子型号"(Claude 3 Opus)
            messages: messages, // 聊天记录(之前说的话)
            max_tokens: 1024, // 军师最多说多少话(防止说起来没完)
            tools: tools // 咱们有的工具,告诉军师
        )
        
        // 4. 把消息体编码成JSON(相当于把奏折写成文言文,军师才懂)
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase // 键名转蛇形(符合API要求)
        request.httpBody = try encoder.encode(body)
        
        // 5. 发消息给军师,等回复(异步等,不然卡着不动)
        let (data, _) = try await URLSession.shared.data(for: request)
        
        // 6. 把军师的回复解码成咱们能懂的格式(文言文转白话文)
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase // 键名转驼峰
        return try decoder.decode(Response.self, from: data)
    }
}

// 发给军师的请求格式(相当于奏折的格式)
struct Request: Encodable {
    let model: String // 军师型号
    let messages: [Message] // 聊天记录
    let max_tokens: Int // 最大回复长度
    let tools: [Tool]? // 可用工具
    
    // 单条消息的格式(谁发的、说啥)
    struct Message: Encodable {
        // 角色:密探(user)还是军师(assistant)
        enum Role: String, Encodable {
            case user = "user" // 密探(用户)
            case assistant = "assistant" // 军师(助手)
        }
        
        let role: Role // 角色
        let content: [Content] // 消息内容(可能是话,也可能是工具调用)
    }
}

// 军师的回复格式(相当于圣旨的格式)
struct Response: Decodable {
    let content: [Content] // 回复内容(可能是话,也可能是让调用工具)
}

// 消息内容的类型(相当于话的种类:说人话、叫工具、工具结果)
enum Content: Codable {
    case text(text: String) // 说人话(比如"我来查查您的发明")
    case toolUse(id: String, name: String, input: [String: String]) // 叫工具(比如"调用查发明的工具")
    case toolResult(toolUseId: String, content: String) // 工具结果(比如"发明状态是XXX")
    
    // 编码解码用的Key(跟军师约定好的名字)
    private enum CodingKeys: String, CodingKey {
        case type, text, id, name, input, tool_use_id, content
    }
    
    // 解码:把军师的回复转成咱们的Content类型
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        
        switch type {
        case "text":
            let text = try container.decode(String.self, forKey: .text)
            self = .text(text: text)
        case "tool_use":
            let id = try container.decode(String.self, forKey: .id)
            let name = try container.decode(String.self, forKey: .name)
            let input = try container.decode([String: String].self, forKey: .input)
            self = .toolUse(id: id, name: name, input: input)
        case "tool_result":
            let toolUseId = try container.decode(String.self, forKey: .tool_use_id)
            let content = try container.decode(String.self, forKey: .content)
            self = .toolResult(toolUseId: toolUseId, content: content)
        default:
            throw ContentError.unknownType // 错误:不知道的内容类型
        }
    }
    
    // 编码:把咱们的Content转成军师能懂的格式
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch self {
        case .text(let text):
            try container.encode("text", forKey: .type)
            try container.encode(text, forKey: .text)
        case .toolUse(let id, let name, let input):
            try container.encode("tool_use", forKey: .type)
            try container.encode(id, forKey: .id)
            try container.encode(name, forKey: .name)
            try container.encode(input, forKey: .input)
        case .toolResult(let toolUseId, let content):
            try container.encode("tool_result", forKey: .type)
            try container.encode(toolUseId, forKey: .tool_use_id)
            try container.encode(content, forKey: .content)
        }
    }
}

// 内容相关的错误
enum ContentError: Error {
    case unknownType // 不知道的内容类型
}

// 军师服务相关的错误
enum AdvisorError: Error {
    case invalidURL // 地址无效
}

零零发边写边念叨:"这 API Key 可得藏好,就像我的'要你命三千'设计图一样,不能让东厂拿去!还有啊,这角色可别搞混了 ------ 你是 user,军师是 assistant,弄反了军师就懵了,跟你和琴操姑娘认错人一样尴尬!"

📱 第四回:搭 "聊天棚"!MCP 客户端的门面

有了服务器和军师,还得有个 "门面"------ 让密探能看见聊天记录,能输入问话。

零零发把这门面叫ContentView,就像他在电影里搭的 "发明展示棚",得直观、好用。

先看 "聊天脑瓜子"(ViewModel)的代码,它负责管聊天逻辑:

swift 复制代码
import SwiftUI
import Observation

// 聊天视图模型(相当于"聊天棚"的脑瓜子,管逻辑)
@Observable
final class InventionChatViewModel {
    var messages: [ChatMessage] = [] // 聊天记录(存所有消息)
    var inputText: String = "" // 输入的话(密探要问的)
    var isLoading: Bool = false // 是不是在加载(等军师回复)
    
    // 依赖:发明服务器和军师服务
    private let inventionServer: MCPServerProtocol
    private let advisorService: ClaudeAdvisorService
    
    // 初始化:把服务器和军师连起来
    init() {
        // 1. 初始化发明服务器(能查发明的)
        self.inventionServer = LingLingFaInventionService()
        // 2. 初始化军师服务(要给API Key和服务器的工具)
        self.advisorService = ClaudeAdvisorService(
            apiKey: "YOUR_API_KEY", // 这里要换成你自己的API Key!
            tools: self.inventionServer.tools
        )
    }
    
    // 发送消息的方法(密探点"发送"就走这儿)
    func sendMessage() {
        // 1. 把密探输入的话做成消息,加到聊天记录里
        let userMessage = Request.Message(
            role: .user,
            content: [.text(text: inputText)] // 密探说的话
        )
        messages.append(ChatMessage(message: userMessage))
        inputText = "" // 清空输入框(说完话总得把嘴擦干净吧)
        isLoading = true // 开始加载(告诉密探:等会儿,军师在想呢)
        
        // 2. 把所有聊天记录整理好,发给军师
        let allMessages = messages.map(\.message)
        
        // 3. 异步发消息(不然卡着不动,跟等皇上批奏折一样)
        Task {
            do {
                // 给军师发消息,等回复
                let advisorResponse = try await advisorService.send(messages: allMessages)
                // 把军师的回复做成消息,加到聊天记录里
                let assistantMessage = ChatMessage(
                    message: Request.Message(
                        role: .assistant,
                        content: advisorResponse.content
                    )
                )
                self.messages.append(assistantMessage)
                
                // 重点:如果军师让调用工具,就去调用(下集细讲!)
                for content in advisorResponse.content {
                    if case .toolUse(let toolId, let toolName, _) = content {
                        // 这里就是调用工具的入口,下集咱们再把这步打通!
                        print("军师让调用工具:\(toolName),ID:\(toolId)")
                    }
                }
            } catch {
                // 出错了,打印错误(实际开发里得提示密探,别让他懵)
                print("糟了!出错了:\(error)")
            }
            isLoading = false // 加载结束(不管成没成,都告诉密探:好了)
        }
    }
}

// 聊天消息结构体(把军师的回复转成密探能看懂的样子)
struct ChatMessage: Identifiable {
    let message: Request.Message // 原始消息
    var id: UUID = .init() // 唯一ID(列表要用)
    
    // 把消息内容转成字符串(密探能直接看的)
    var contentText: String {
        message.content
            .map { content in
                switch content {
                case .text(let text):
                    return text // 人话直接返回
                case .toolUse(_, let toolName, _):
                    return "军师调用工具:\(toolName)(查发明状态中...)"
                case .toolResult(_, let result):
                    return "工具返回结果:\(result)"
                }
            }
            .joined(separator: "\n") // 多条内容换行
    }
}

零零发指着屏幕说:"你看这 UI,密探的消息放右边,军师的放左边,跟茶馆里俩人对坐聊天一样!输入框能写三行,问复杂的问题也不怕,按钮亮着就能点 ------ 比我那'要你命三千'的开关好懂多了!"

🎬 尾声:神器初成,就差最后一步!

话说零零发把 "密探发明聊天棚" 搭好了:能查发明的服务器立住了,能聊天的军师也接上了,UI 门面也弄得明明白白。

现在你要是打开 APP,输入 "查下我那天外飞仙破解器的状态",军师会说 "我来调用工具查查",可查完之后呢?工具的结果还没传给军师,军师也没法给你个完整的答复 ------ 这就像 "要你命三千" 装好了刀片,却没按启动键,砍不了人啊!

下一回,咱们就来打通这最后一步:**让工具调用的结果自动传给军师,军师再给你个清清楚楚的回复。**到时候,你问一句 "我的发明咋样了",神器就能自动查、自动说,比零零发的老婆还贴心!各位客官,咱们下集接着唠,不见不散~

相关推荐
大熊猫侯佩3 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(下)
llm·ai编程·mcp
下位子3 小时前
『AI 编程』用 Claude Code 从零到一开发全栈减脂追踪应用
前端·ai编程·claude
子昕3 小时前
Claude Code插件系统上线!AI编程的“App Store”时代来了
ai编程
Java中文社群4 小时前
n8n和在线免费体验蚂蚁万亿开源大模型Ling-1T!
aigc·ai编程
302AI4 小时前
体验升级而非颠覆,API成本直降75%:DeepSeek-V3.2-Exp评测
人工智能·llm·deepseek
聚客AI6 小时前
🥺单智能体总是翻车?可能是你缺了这份LangGraph多Agent架构指南
人工智能·llm·agent
yaocheng的ai分身6 小时前
氛围编码革命进入下一阶段: Bolt v2
ai编程
爱可生开源社区6 小时前
2025 年 9 月《大模型 SQL 能力排行榜》发布,新增 Kimi K2 最新版测评!
sql·llm
大模型教程6 小时前
半小时部署企业智能问答系统!MaxKB让知识管理效率翻倍
程序员·llm·agent