Récemment, des utilisateurs sur X et Reddit ont rapporté qu'écrire du code avec Claude 3.5 Sonnet leur avait permis de se sentir « très puissants ». L'un de ces messages qui a particulièrement attiré l'attention était un tweet du PDG de YCombinator, Garry Tan.
Garry a partagé le message suivant rédigé par l'un des membres de la communauté YCombinator Reddit sur la façon dont l'utilisation de Claude 3.5 Sonnet a multiplié par 10 leur productivité tout en implémentant des fonctionnalités populaires.
Le message a également souligné l'importance de prendre des décisions architecturales et infrastructurelles judicieuses en tant qu'élément crucial de leur utilisation quotidienne du LLM.
Bien que les LLM comme Claude 3.5 offrent des avantages exceptionnels, ils présentent encore des limites en termes de conservation de la mémoire et du contexte. Plusieurs stratégies de développement peuvent être utilisées pour remédier à ces limitations. Ces stratégies s'articulent autour des bases du développement logiciel que tous les développeurs perfectionnent avec leur expérience, mais qu'il est souvent facile de manquer lorsqu'ils proposent des LLM en anglais simple.
L'application des mêmes principes de base que les développeurs utilisent quotidiennement, ce que l'auteur de Reddit appelle des décisions architecturales, aux interactions LLM peut aboutir à un code hautement modulaire, évolutif et bien documenté.
Voici quelques principes de codage et pratiques de développement clés qui peuvent être appliqués au développement de logiciels assisté par LLM, ainsi que des exemples pratiques en Python :
Toutes les invites des exemples ont été utilisées avec Claude Sonnet 3.5.
Pour faciliter la description de chaque composant logique dans le LLM et obtenir des composants précis, divisez la base de code en petits composants bien définis.
# database.py class Database: def __init__(self, sql_connection_string): .... def query(self, sql): .... # user_service.py class UserService: def __init__(self, database): self.db = database def get_user(self, user_id): return self.db.query(f"SELECT * FROM users WHERE id = {user_id}") # main.py db = Database("sql_connection_string") user_service = UserService(db) user = user_service.get_user(123)
Pour masquer les implémentations complexes, utilisez des couches d'abstraction et concentrez-vous sur les abstractions de niveau supérieur en utilisant les détails de chaque composant enregistrés en cours de route.
# top-level abstraction class BankingSystem: def __init__(self): self._account_manager = AccountManager() self._transaction_processor = TransactionProcessor() def create_account(self, acct_number: str, owner: str) -> None: self._account_manager.create_account(acct_number, owner) def process_transaction(self, acct_number: str, transaction_type: str, amount: float) -> None: account = self._account_manager.get_account(acct_number) self._transaction_processor.process(account, transaction_type, amount) # mid-level abstractions class AccountManager: def __init__(self): def create_account(self, acct_number: str, owner: str) -> None: def get_account(self, acct_number: str) -> 'Account': class TransactionProcessor: def process(self, account: 'Account', transaction_type: str, amount: float) -> None: # lower-level abstractions class Account(ABC): .... class Transaction(ABC): .... # concrete implementations class SavingsAccount(Account): .... class CheckingAccount(Account): .... class DepositTransaction(Transaction): .... class WithdrawalTransaction(Transaction): .... # lowest-level abstraction class TransactionLog: .... # usage focuses on the high-level abstraction cart = ShoppingCart() cart.add_item(Item("Book", 15)) cart.add_item(Item("Pen", 2)) total = cart.get_total()
Pour rendre les tâches plus faciles à gérer lorsque vous demandez l'aide de LLM, définissez des interfaces claires pour chaque composant et concentrez-vous sur leur mise en œuvre séparée.
class PaymentProcessor(ABC): @abstractmethod def process_payment(self, amount: float, card_no: str) -> bool: .... class StripeProcessor(PaymentProcessor): # stripe specific implementation def process_payment(self, amount: float, card_no: str) -> bool: .... class PayPalProcessor(PaymentProcessor): # paypal specific implementation def process_payment(self, amount: float, card_no: str) -> bool: ....
Pour éviter les hallucinations et limiter la portée, concentrez-vous sur un petit élément à la fois et assurez-vous que chaque classe/fonction a une responsabilité unique et bien définie. Développez progressivement pour avoir plus de contrôle sur le code généré.
class UserManager: # user creation logic def create_user(self, username, email): ... class EmailService: # send welcome email logic def send_welcome_email(self, email): .... class NotificationService: # send sms notification def send_sms(self, username, email): ... # Usage user_manager = UserManager() email_svc = EmailService() user = user_manager.create_user("hacker", "[email protected]") email_svc.send_welcome_email("[email protected]")
Pour simplifier la description de la structure du code aux LLM et comprendre leurs suggestions, utilisez des règles de dénomination claires et cohérentes.
# classes: PascalCase class UserAccount: pass # functions and variables: snake_case def calculate_total_price(item_price, quantity): total_cost = item_price * quantity return total_cost # constants: UPPERCASE_WITH_UNDERSCORES MAX_LOGIN_ATTEMPTS = 3 # private methods/variables: prefix with underscore class DatabaseConnection: def __init__(self): self._connection = None def _connect_to_database(self): pass
Pour générer des implémentations spécifiques en fonction des exigences, créez un code squelette pour les structures de code courantes et utilisez-les comme code de démarrage.
# Todo item - pydantic Model class TodoItem(BaseModel): id: Optional[int] = None title: str description: Optional[str] = None completed: bool = False todo_db = [<insert mock data>] @app.get("/todos", response_model=List[TodoItem]) async def get_todos(): # TODO: retrieve all todos @app.get("/todos/{todo_id}", response_model=TodoItem) async def get_todo(todo_id: int): # TODO: retrieve specific todo @app.post("/todos", response_model=TodoItem) async def create_todo(todo: TodoItem): # TODO: create todo @app.put("/todos/{todo_id}", response_model=TodoItem) async def update_todo(todo_id: int, todo: TodoItem): # TODO: update todo @app.delete("/todos/{todo_id}") async def delete_todo(todo_id: int): # TODO: delete todo
Pour aider à guider le LLM, rédigez une documentation concise pour chaque composant avant la mise en œuvre. De plus, générez des docstrings qui peuvent guider les invites à l'avenir en utilisant le LLM lui-même.
rapide:
Use a Documentation Driven Development approach to write clear, concise docstrings for the following python function calculate_discount(price, discount_percentage) takes in - price (float), original price of the item. - discount_percentage (float), discount percentage range(0-100). returns - price after discount (float) raises - ValueError, when discount_percentage is out of range example - calculate_discount(100, 10) -> 10.0
réponse:
def calculate_discount(price: float, discount_percent: float) -> float: """ Calculate the discounted price of an item. Args: price (float): original price for the item. discount_percentage (float): discount percentage range(0-100). Returns: float: The price after applying the discount. Raises: ValueError: If discount_percentage is not between 0 and 100. Example: >>> calculate_discount(100, 20) 80.0 """ if not 0 <= discount_percent <= 100: raise ValueError("Discount percent must be between 0 and 100") discount_amount = price * (discount_percent / 100) return price - discount_amount
Pour aider les LLM dans la mise en œuvre et en particulier gérer les cas extrêmes, écrivez vous-même des tests ou utilisez les LLM pour le faire avant d'implémenter la fonction réelle. Les LLM n'exécutent pas de code en interne, mais effectuer des tests au préalable pour garantir que le code généré répond aux exigences de la fonction peut être utile pour détecter les bogues dans le code généré par LLM.
cas de test:
def is_palindrome(s: str) -> bool: """check if string is palindrome.""" return s == s[::-1] class TestPalindrome(unittest.TestCase): def test_palindrome(self): self.assertTrue(is_palindrome("racecar")) self.assertTrue(is_palindrome("A man a plan a canal Panama")) self.assertFalse(is_palindrome("hello")) self.assertTrue(is_palindrome("")) # Edge case: empty string
rapide:
given the test case <insert test>, use Document Driven Development to write a function for TestPalindrome
réponse:
def is_palindrome(s: str) -> bool: """ Check if a string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward, ignoring spaces, punctuation, and capitalization. Args: s (str): The input string to check. Returns: bool: True if the input is a palindrome, False otherwise. Examples: >>> is_palindrome("racecar") True >>> is_palindrome("A man a plan a canal Panama") True >>> is_palindrome("hello") False >>> is_palindrome("") True """ # Remove non-alphanumeric characters and convert to lowercase cleaned = ''.join(char.lower() for char in s if char.isalnum()) # Compare the cleaned string with its reverse return cleaned == cleaned[::-1]
Pour discuter de plusieurs détails de mise en œuvre avec le LLM, conservez une bibliothèque de modèles de conception couramment utilisés à titre de référence.
# Singleton pattern class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) # initialize database connection return cls._instance # Factory pattern class AnimalFactory: @staticmethod def create_animal(animal_type): if animal_type == "dog": return Dog() elif animal_type == "cat": return Cat() else: raise ValueError("Unknown animal type") # Observer pattern class Subject: def __init__(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self): for observer in self._observers: observer.update() # Adapter pattern class LLMAdapter: def __init__(self, llm_service): self.llm_service = llm_service def generate_code(self, prompt): llm_response = self.llm_service.complete(prompt) return self.extract_code(llm_response) def extract_code(self, response): pass
Pour garantir la qualité et la cohérence, créez une liste de contrôle pour examiner le code généré par LLM.
# Code Review Checklist ## Functionality - [ ] Code performs the intended task correctly - [ ] Edge cases are handled appropriately ## Code Quality - [ ] Code follows project's style guide - [ ] Variable and function names are descriptive and consistent - [ ] No unnecessary comments or dead code ## Performance - [ ] Code is optimized for efficiency - [ ] No potential performance bottlenecks ## Security - [ ] Input validation is implemented - [ ] Sensitive data is handled securely ## Testing - [ ] Unit tests are included and pass - [ ] Edge cases are covered in tests ## Documentation - [ ] Functions and classes are properly documented - [ ] Complex logic is explained in comments
Les LLM fonctionnent mieux avec une structure définie, alors développez une stratégie pour diviser les tâches de codage en invites plus petites. Suivre une approche organisée permet de générer un code fonctionnel sans demander au LLM de re-corriger le code généré plusieurs fois.
rapide:
I need to implement a function to calculate the Fibonacci number sequence using a Document Driven Development approach. 1. Purpose: function that generates the Fibonacci sequence up to a given number of terms. 2. Interface: def fibonacci_seq(n: int) -> List[int]: """ generate Fibonacci sequence up to n terms. Args: n (int): number of terms in the sequence Returns: List[int]: fibonacci sequence """ 3. Key Functionalities: - handle input validation (n should always be a positive integer) - generate the sequence starting with 0 and 1 - each subsequent number is the sum of two preceding ones - return the sequence as a list 4. Implementation Details: - use a loop to generate the sequence - store the sequence in a list - optimize for memory by only keeping the last two numbers in memory if needed 5. Test Cases: - fibonacci_seq(0) should return [] - fibonacci_seq(1) should return [0] - fibonacci_seq(5) should return [0, 1, 1, 2, 3]
Bien que tous les exemples ci-dessus puissent sembler simples, suivre des pratiques fondamentales telles que l'architecture modulaire et une ingénierie rapide efficace, et adopter une approche structurée solide dans le développement assisté par LLM, fait une grande différence à grande échelle. En mettant en œuvre ces pratiques, davantage de développeurs peuvent maximiser les avantages de l'utilisation des LLM, ce qui se traduit par une productivité et une qualité de code améliorées.
Les LLM sont des outils puissants qui fonctionnent mieux lorsqu'ils sont guidés par des principes de génie logiciel faciles à ignorer. Leur internalisation pourrait faire la différence entre un code élégamment conçu et une Big Ball of Mud générée aléatoirement.
L'objectif de cet article est d'encourager les développeurs à toujours garder ces pratiques à l'esprit lorsqu'ils utilisent les LLM pour produire du code de haute qualité et gagner du temps à l'avenir. À mesure que les LLM continuent de s’améliorer, les fondamentaux deviendront encore plus cruciaux pour en tirer le meilleur parti.
Pour une analyse plus approfondie des principes de développement logiciel, consultez ce manuel classique : Clean Architecture : A Craftsman's Guide to Software Structure and Design de Robert C. Martin.
Si vous avez apprécié cet article, restez à l'écoute du prochain dans lequel nous approfondirons un flux de travail détaillé pour le développement assisté par LLM. Veuillez partager tout autre concept que vous jugez important dans les commentaires ! Merci.