Я снова оказался в сценарии написания глупого шаблонного кода. Поскольку мне это надоело, так как была заметна четкая схема, я решил попробовать ChatGPT.

Представим сценарий: совсем недавно я программировал интеграцию спецификации GTFS в API для небольшого местного транспортного агентства. Я знаю, что для этого есть вполне подходящие библиотеки, вроде рекомендованных здесь, но они раздуты, и мне не имеет смысла включать их в свой легковесный сервер. Поэтому я начал строить модели данных, представляющие части спецификации, пока не вспомнил, что есть верный помощник (если онлайн и не перегружен) — ChatGPT!

Так какая была задача? Вкратце, GTFS — это простой набор текстовых файлов, заполненных информацией о транзите и обычно предоставляемых в виде архива *.zip. Я хотел иметь представление каждого файла в своем приложении, чтобы предоставлять конечным пользователям только его части в нескольких форматах, таких как JSON или XML, а не только как текст.

Вот фрагмент кода, который я написал для одного из файлов — Agency.txt. Чтобы сделать его более удобным для сопровождения для других (после завершения этот проект будет с открытым исходным кодом), я включил официальную документацию для каждого поля, как указано в этой справке по GTFS.

/**
 * @author nlehmann
 * Transit agencies with service represented in this dataset.
 * @see <a href="https://gtfs.org/schedule/reference/#agencytxt">...</a>
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Agency {


    /**
     * Identifies a transit brand which is often synonymous with a transit agency.
     * Note that in some cases, such as when a single agency operates multiple separate services, agencies and brands are distinct.
     * A dataset may contain data from multiple agencies.
     * Conditionally Required:
     * - Required when the dataset contains data for multiple transit agencies.
     * - Optional otherwise.
     */
    private String agencyId;

    /**
     * Full name of the transit agency.
     */
    private String agencyName;

[...]

Что меня поразило в первую очередь, так это то, что копирование всей этой справочной документации для полей было рутинной задачей, которую мог бы выполнить любой программист, и это заняло целую вечность! Поэтому я проверил, были ли какие-либо критические обновления в спецификации GTFS с 2021 года. Почему? вы спрашиваете? Простой, ChatGPT обучается только на данных до 2021 года. Поскольку с тех пор спецификация никогда не обновлялась (GTFS проста), я открыл новый чат и попытал счастья.

И вот что произошло:

We're experiencing exceptionally high demand. Please hang tight as we 
work on scaling our systems.

Конец.

ОК — просто пошутил. Вот исходная задача, которую я дал ChatGPT:

I am converting GTFS Files to their respective representation in Java, 
including documentation originating in the original reference of the GTFS 
website. Can you convert me the stops.txt to Java with the following 
template of the same from the agency.txt:
package de.noahlehmann.hofbusapi.util.gtfs.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @author nlehmann
 * Transit agencies with service represented in this dataset.
 * @see <a href="https://gtfs.org/schedule/reference/#agencytxt">GTFS reference for GTFS schedule</a>
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Agency {


    /**
     * Identifies a transit brand which is often synonymous with a transit agency.
     * Note that in some cases, such as when a single agency operates multiple separate services, agencies and brands are distinct.
     * A dataset may contain data from multiple agencies.
     * Conditionally Required:
     * - Required when the dataset contains data for multiple transit agencies.
     * - Optional otherwise.
     */
    private String agencyId;

    /**
     * Full name of the transit agency.
     */
    private String agencyName;

    /**
     * URL of the transit agency.
     */
    private String agencyUrl;

    /**
     * Timezone where the transit agency is located.
     * If multiple agencies are specified in the dataset, each must have the same agency_timezone.
     */
    private String agencyTimezone;

    /**
     * Primary language used by this transit agency.
     * Should be provided to help GTFS consumers choose capitalization rules and other language-specific settings for the dataset.
     */
    private String agencyLang;

    /**
     * A voice telephone number for the specified agency.
     * This field is a string value that presents the telephone number as typical for the agency's service area.
     * It may contain punctuation marks to group the digits of the number.
     * Dialable text (for example, TriMet's "503-238-RIDE") is permitted, but the field must not contain any other descriptive text.
     */
    private String agencyPhone;

    /**
     * URL of a web page that allows a rider to purchase tickets or other fare instruments for that agency online.
     */
    private String agencyFareUrl;

    /**
     * Email address actively monitored by the agency’s customer service department.
     * This email address should be a direct contact point where transit riders can reach a customer service representative at the agency.
     */
    private String agencyEmail;

}

Это утешительные слова, которыми ChatGPT приветствовал меня:

Конечно, вот пример классаStopв Java по шаблону, предоставленному для классаAgency: […]

И тут дело пошло:

package de.noahlehmann.hofbusapi.util.gtfs.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @author nlehmann
 * Transit stops represented in this dataset.
 * @see <a href="https://gtfs.org/schedule/reference/#stopstxt">GTFS reference for GTFS schedule</a>
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Stop {

    /**
     * Identifies a stop or station.
     * Must be unique for each stop or station within a service area.
     * Conditionally Required:
     * - Required for all stops or stations.
     */
    private String stopId;

    /**
     * Name of the stop or station.
     * Conditionally Required:
     * - Required for all stops or stations.
     */
    private String stopName;

    /**
     * Description of the stop or station.
     */
    private String stopDesc;

    /**
     * Latitude of the stop or station.
     * Conditionally Required:
     * - Required for all stops or stations.
     */
    private Double stopLat;

    /**
     * Longitude of the stop or station.
     * Conditionally Required:
     * - Required for all stops or stations.
     */
    private Double stopLon;

    /**
     * Zone identifier for the stop or station.
     * This can be used in conjunction with the fare attribute field in the fare_rules.txt file to define fare zones.
     */
    private String stopCode;

    /**
     * URL of a web page about a particular stop or station.
     */
    private String stopUrl;

    /**
     * Type of location.
     * Conditionally Required:
     * - Required for all stops or stations.
     */
    private String locationType;

    /**
     * Identifies the parent station of a stop.
     * If this stop is a parent station, this value is empty.
     */
    private String parentStation;

    /**
     * Identifies the timezone of the stop or station.
     * This timezone is used to calculate time-dependent information, such as the arrival and departure times of a particular trip at this stop or station.
     */
    private String stopTimezone;

    /**
     * Identifies the wheelchair accessibility of the stop or station.
     * This field can be used to indicate whether a stop or station is wheelchair accessible.
     */
    private String wheelchairBoarding;

}

Это впечатляет. Я имею в виду, не поймите меня неправильно, эта задача проста, но я никогда не сообщал, где именно найти документацию. Впечатляет, продолжим:

Can you do the same for the routes.txt. The output seems to be faultless.

Обратите внимание на последнее предложение, я должен быть исправлен. Вот ответ:

Sure, here is an example of the Route class in Java, following the template 
provided for the Stop class:
public class Route {

    /**
     * Identifies a route.
     * Must be unique for each route for each agency.
     * Conditionally Required:
     * - Required for all routes.
     */
    private String routeId;

    /**
     * Short name of the route.
     * This will often be a short, abstract identifier like "32", "100X", or "Green" that riders use to identify a route,
     * but which doesn't give any indication of what places the route serves.
     * Conditionally Required:
     * - Required for all routes.
     */
    private String routeShortName;

[...]

Обратите внимание на части JavaDoc в конце описания каждой переменной.

/**
* Conditionally Required:
* - Required for all routes.
*/

Это верно не для всех полей, тем не менее кажется, что ChatGPT вставляет это везде. Также отсутствует самое важное поле, представляющее ссылку на другой файл. Поэтому я говорю ChatGPT:

You missed the agency_id field. Also the javadocs required notes are not 
the same as in the documentation of GTFS.

А ChatGPT просто принимает мой ответ и приносит свои извинения…

You are correct, I apologize for the oversight. Here is an updated version 
of the Route class that includes the agency_id field and the javadocs for 
the required fields match the GTFS documentation:

И доставляет!

/**
  * Identifies a transit agency.
  * Must be unique for each agency and referenced by all routes.
  * Conditionally Required:
  * - Required when the dataset contains data for multiple transit agencies.
  * - Optional otherwise.
  */
private String agencyId;

Довольно впечатляющий. Однако стоит отметить одну вещь, особенно если вы используете ChatGPT впервые или не совсем понимаете, как он работает внутри. ChatGPT знает только те данные, которые ему передали, но это могли быть любые данные, в том числе и неправильные данные. Затем ChatGPT пытается найти смысл в том, что он читает и пишет, поэтому кажется, что он увидел шаблоны в требованиях, упомянутых в JavaDocs, что было неверно. Поэтому мой совет: ВСЕГДА перепроверяйте ответы ChatGPT, прежде чем их использовать, поскольку они регулярно будут ошибочными.

Но после небольшой настройки вывода на некоторое время и предоставления ChatGPT необходимой обратной связи все файлы были преобразованы в простые старые объекты Java (POJO), и я доволен этим. Это заняло бы у меня 2 часа легко, и это было бы скучно. Вместо этого мне потребовалось всего 20 минут и некоторая вычитка.

Поэтому я подумал, почему бы не проверить это на похоже структурированной части кода, конечных точках API. Имейте в виду, что они написаны с использованием Spring Web MVC в качестве фреймворка, но схема здесь не имеет большого значения. Вот вопрос:

I have a getmapping method in a spring restcontroller class that looks 
the following:
@GetMapping("/agency") 
public ResponseEntity<Object> getAgency(){ 
    return ResponseEntity.ok(null); 
}
Can you generate me this method for all other required, conditionally 
required and recommended GTFS file types.

И это так!

Sure, here are examples of @GetMapping methods for all other required, 
conditionally required and recommended GTFS file types:

    @GetMapping("/stops")
    public ResponseEntity<Object> getStops(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/routes")
    public ResponseEntity<Object> getRoutes(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/trips")
    public ResponseEntity<Object> getTrips(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/calendar")
    public ResponseEntity<Object> getCalendar(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/calendarDates")
    public ResponseEntity<Object> getCalendarDates(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/stopTimes")
    public ResponseEntity<Object> getStopTimes(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/frequencies")
    public ResponseEntity<Object> getFrequencies(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/transfers")
    public ResponseEntity<Object> getTransfers(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/feedInfo")
    public ResponseEntity<Object> getFeedInfo(){
        return ResponseEntity.ok(null);
    }

После небольшой доработки…

Can you rewrite this to only use lowercase for the endpoints and only 
singular for the words?
Could you do the same lowercase methods in plural again, if applicable?
Please only use plural, if the filename does so too.

И наконец:

Please only print the required, conditionally required and recommended file 
types. Also, please make the return type Object of the response entity 
either a list or the single type of a java class representing the filename 
served by the endpoint, only being plural, if the filename is.

Это было довольно расплывчато, учитывая заданные требования. Не могли бы вы определить, что я хотел? Ну, ChatGPT сделал!

Sure, here are the examples of the @GetMapping methods with lowercase 
endpoint names, matching the plural nouns of the corresponding required, 
conditionally required and recommended GTFS file types, with the return type 
as a list or the single type of java class, representing the filename served 
by the endpoint:
    @GetMapping("/agencies")
    public ResponseEntity<List<Agency>> getAgencies(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/stops")
    public ResponseEntity<List<Stop>> getStops(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/routes")
    public ResponseEntity<List<Route>> getRoutes(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/trips")
    public ResponseEntity<List<Trip>> getTrips(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/calendar")
    public ResponseEntity<List<Calendar>> getCalendar(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/calendar_dates")
    public ResponseEntity<List<CalendarDate>> getCalendarDates(){
        return ResponseEntity.ok(null);
    }

    @GetMapping("/stop_times")
    public ResponseEntity<List<StopTime>> getStopTimes(){
        return ResponseEntity.ok(null);
    }

И снова это выглядит многообещающе, но есть одна загвоздка, так что проверяйте ответы!

(Me)
Feed_info is recommended though as specified by google in their developer 
guidelines.
(ChatGPT)
I apologize for the confusion, you are correct that feed_info.txt is 
recommended by Google in their GTFS developer guidelines. Here is an 
example of the @GetMapping method for feed_info.txt:
@GetMapping("/feed_info")
public ResponseEntity<FeedInfo> getFeedInfo(){
    return ResponseEntity.ok(null);
}

Наконец, эти конечные точки API едва ли были бы полезны, если бы пользователь не мог фильтровать по идентификатору, ссылающемуся на другие файлы. Поэтому я спрашиваю это:

If applicable, could you insert request parameters provided as query 
parameters for those endpoints, that can be filtered by one column of the 
file referencing an id of another file, like for instance the stop times for 
a trip id?

И снова ChatGPT обеспечивает:

@GetMapping("/stop_times")
public ResponseEntity<List<StopTime>> getStopTimes(
        @RequestParam(value = "trip_id", required = false) String tripId) {
    // You can use the tripId to filter the stop times by the trip_id 
    // column in the stop_times table
    return ResponseEntity.ok(null);
}

Довольно круто. Хотя я должен сказать, что на данный момент просьба ChatGPT сделать это для всех конечных точек также дала мне несколько бессмысленных ответов, поэтому еще раз проверьте свои ответы. И в этот момент я исправлял ответы ChatGPT так часто, что самому добавлять параметры запроса фильтра было просто быстрее — так я и сделал.

Тем не менее этот пример показывает потенциальную выгоду от ChatGPT. Я согласен с тем, что некоторые люди называют это страшным — это так! Но инструмент уже есть, так почему бы им не воспользоваться?

Получайте удовольствие, пробуя то же самое! Надеюсь, вам понравился мой небольшой опыт здесь.

P.S. На самом деле я сам написал эту статью ;)